import "core-js/stable"
import "regenerator-runtime/runtime"
/*!
* jQuery Timeline
* ------------------------
* Version: 2.1.3
* Author: Ka2 (https://ka2.org/)
* Repository: https://github.com/ka215/jquery.timeline
* Lisenced: MIT
*/
/* ----------------------------------------------------------------------------------------------------------------
* Constants
* ----------------------------------------------------------------------------------------------------------------
*/
const NAME = "Timeline"
const VERSION = "2.1.3"
const DATA_KEY = "jq.timeline"
const EVENT_KEY = `.${DATA_KEY}`
const PREFIX = "jqtl-"
const IS_TOUCH = 'ontouchstart' in window
const MIN_POINTER_SIZE = 12
const JQUERY_NO_CONFLICT = $.fn[NAME]
const Default = {
type : "bar",
scale : "day",
startDatetime : "currently",
endDatetime : "auto",
range : 3,
rows : "auto",
rowHeight : 48,
width : "auto",
height : "auto",
minGridSize : 30,
marginHeight : 2,
headline : {
display : true,
title : "",
range : true,
locale : "en-US",
format : { hour12: false }
},
sidebar : {
sticky : false,
overlay : false,
list : [],
},
ruler : {
truncateLowers : false,
top : {
lines : [],
height : 30,
fontSize : 14,
color : "inherit",// Changed since v2.1.0, an old value is "#777777"
background : "inherit",// Changed since v2.1.0, an old value is "#FFFFFF"
locale : "en-US",
format : { hour12: false }
},
bottom : {// Added since v2.1.0 to fix #61
lines : [],
height : 30,
fontSize : 14,
color : "inherit",
background : "inherit",
locale : "en-US",
format : { hour12: false }
}
},
footer : {
display : true,
content : "",
range : false,
locale : "en-US",
format : { hour12: false }
},
eventMeta : {
display : false,
scale : "day",
locale : "en-US",
format : { hour12: false },
content : ""
},
eventData : [],
effects : {
presentTime : true,
hoverEvent : true,
stripedGridRow : true,
horizontalGridStyle : "dotted",
verticalGridStyle : "solid",
},
//startHour : 0, // Merge PR#37
//endHour : 23, // Merge PR#37
/*
hourPeriod : { // Added new option inspired from PR#37; Available only if the scale is "day"( or "week")
start : 0,
end : 23,
},
*/
colorScheme : { // Added new option since v2.0.0
theme : { // Added new option since v2.1.0
name : "default",
text : "#343A40",
subtext : "#707070",
offtext : "#BBBBBB",
modesttext : "#969696",
line : "#6C757D",
offline : "#DDDDDD",
activeline : "#DC3545",
background : "#FFFFFF",
invertbg : "#121212",
striped1 : "#F7F7F7",
striped2 : "#F0F0F0",
active : "#F73333",
marker : "#2C7CFF",
gridbase : "#333333"
},
event : {
text : "#343A40",
border : "#6C757D",
background : "#E7E7E7"
},
hookEventColors : () => null, // Added instead of merging setColorEvent of PR#37 since v2.0.0
},
// onOpenEvent : () => null, // Merge PR#37; (event) => null
rangeAlign : "latest",
loader : "default",
loadingMessage : "",
hideScrollbar : false,
storage : "session",
reloadCacheKeep : true,
zoom : false,
wrapScale : true,
firstDayOfWeek : 0, // 0: Sunday, 1: Monday, ... 6: Saturday
engine : "canvas",
disableLimitter : false, // Added new option since v2.1.2
debug : false,
// datetimeFormat : {}, // --> Deprecated since v2.0.0
// datetimePrefix : "", // --> Deprecated since v2.0.0
// duration : 150, // --> Deprecated since v2.0.0
// showPointer : true, // --> Deprecated since v2.0.0
// httpLanguage : false, // --> Deprecated since v2.0.0
// i18n : {}, // --> Deprecated since v2.0.0
// langsDir : "./langs/", // --> Deprecated since v2.0.0
// minGridPer : 2, // --> Deprecated since v2.0.0
// minuteInterval : 30, // --> Deprecated since v2.0.0
// naviIcon : {}, // --> Deprecated since v2.0.0
// showHeadline : true, // --> Deprecated since v2.0.0
// zerofillYear : false, // --> Deprecated since v2.0.0
}
const LimitScaleGrids = {
millennium : 100,
century : 100 * 5,
decade : 10 * 50,
lustrum : 5 * 100,
year : 500,
month : 12 * 45,
week : 53 * 10,
day : 366,
hour : 24 * 30,
quarterHour : 24 * 4 * 7.5,
halfHour : 24 * 2 * 15,
minute : 60 * 12,
second : 60 * 15
}
const EventParams = {
uid : "",
eventId : "",
x : 0,
y : Default.marginHeight,
width : Default.minGridSize,
height : Default.rowHeight - Default.marginHeight * 2,
start : "",
end : "",
row : 1,
bgColor : Default.colorScheme.event.background, // Modified since v2.0.0
color : Default.colorScheme.event.text, // Modified since v2.0.0
bdColor : Default.colorScheme.event.border, // Modified since v2.0.0
label : "",
content : "",
category : "", // Added new option since v2.0.0
image : "",
margin : Default.marginHeight,
rangeMeta : "",
size : "normal",
type : "",
extend : {},
remote : false,
relation : {},
callback() {}
}
const Event = {
INITIALIZED : `initialized${EVENT_KEY}`,
HIDE : `hide${EVENT_KEY}`,
SHOW : `show${EVENT_KEY}`,
CLICK_EVENT : `click.open${EVENT_KEY}`,
FOCUSIN_EVENT : `focusin.event${EVENT_KEY}`,
FOCUSOUT_EVENT : `focusout.event${EVENT_KEY}`,
TOUCHSTART_TIMELINE: `mousedown.timeline${EVENT_KEY}`, // Deleted touchstart.timeline${EVENT_KEY} because never work the openEvent on touch device; since v2.0.0
TOUCHMOVE_TIMELINE : `mousemove.timeline${EVENT_KEY}`, // Deleted touchmove.timeline${EVENT_KEY} because never work the openEvent on touch device; since v2.0.0
TOUCHEND_TIMELINE : `mouseup.timeline${EVENT_KEY}`, // Deleted touchend.timeline${EVENT_KEY} because never work the openEvent on touch device; since v2.0.0
MOUSEENTER_POINTER : `mouseenter.pointer${EVENT_KEY}`,
MOUSELEAVE_POINTER : `mouseleave.pointer${EVENT_KEY}`,
ZOOMIN_SCALE : `dblclick.zoom${EVENT_KEY}`
}
const ClassName = {
TIMELINE_CONTAINER : `${PREFIX}container`,
TIMELINE_MAIN : `${PREFIX}main`,
TIMELINE_HEADLINE : `${PREFIX}headline`,
TIMELINE_HEADLINE_WRAPPER : `${PREFIX}headline-wrapper`,
HEADLINE_TITLE : `${PREFIX}timeline-title`,
RANGE_META : `${PREFIX}range-meta`,
RANGE_SPAN : `${PREFIX}range-span`,
TIMELINE_EVENT_CONTAINER : `${PREFIX}event-container`,
TIMELINE_BACKGROUND_GRID : `${PREFIX}bg-grid`,
TIMELINE_RELATION_LINES : `${PREFIX}relation-lines`,
TIMELINE_EVENTS : `${PREFIX}events`,
TIMELINE_EVENT_NODE : `${PREFIX}event-node`,
TIMELINE_EVENT_LABEL : `${PREFIX}event-label`,
TIMELINE_EVENT_THUMBNAIL : `${PREFIX}event-thumbnail`,
TIMELINE_RULER_LINES : `${PREFIX}ruler-line-rows`,
TIMELINE_RULER_ITEM : `${PREFIX}ruler-line-item`,
TIMELINE_SIDEBAR : `${PREFIX}side-index`,
TIMELINE_SIDEBAR_MARGIN : `${PREFIX}side-index-margin`,
TIMELINE_SIDEBAR_ITEM : `${PREFIX}side-index-item`,
TIMELINE_FOOTER : `${PREFIX}footer`,
TIMELINE_FOOTER_CONTENT : `${PREFIX}footer-content`,
VIEWER_EVENT_TITLE : `${PREFIX}event-title`,
VIEWER_EVENT_CONTENT : `${PREFIX}event-content`,
VIEWER_EVENT_META : `${PREFIX}event-meta`,
VIEWER_EVENT_IMAGE_WRAPPER : `${PREFIX}event-image-wrapper`,
VIEWER_EVENT_IMAGE : `${PREFIX}event-image`,
VIEWER_EVENT_TYPE_POINTER : `${PREFIX}event-type-pointer`,
HIDE_SCROLLBAR : `${PREFIX}hide-scrollbar`,
HIDE : `${PREFIX}hide`,
RULER_ITEM_ALIGN_LEFT : `${PREFIX}rli-left`,
STICKY_LEFT : `${PREFIX}sticky-left`,
OVERLAY : `${PREFIX}overlay`,
ALIGN_SELF_RIGHT : `${PREFIX}align-self-right`,
PRESENT_TIME_MARKER : `${PREFIX}present-time`,
LOADER_CONTAINER : `${PREFIX}loader`,
LOADER_ITEM : `${PREFIX}loading`
}
const Selector = {
EVENT_NODE : `.${PREFIX}event-node`,
EVENT_VIEW : `.timeline-event-view, .${PREFIX}event-view`,
RULER_TOP : `.${PREFIX}ruler-top`,
RULER_BOTTOM : `.${PREFIX}ruler-bottom`,
TIMELINE_CONTAINER : `.${ClassName.TIMELINE_CONTAINER}`,
TIMELINE_MAIN : `.${ClassName.TIMELINE_MAIN}`,
HEADLINE_TITLE : `.${ClassName.HEADLINE_TITLE}`,// Added since v2.1.0
RANGE_META : `.${ClassName.RANGE_META}`,// Added since v2.1.0
TIMELINE_RULER_TOP : `.${PREFIX}ruler-top`,
TIMELINE_EVENT_CONTAINER : `.${ClassName.TIMELINE_EVENT_CONTAINER}`,
TIMELINE_RULER_BOTTOM : `.${PREFIX}ruler-bottom`,
TIMELINE_RULER_LINES : `.${ClassName.TIMELINE_RULER_LINES}`,// Added since v2.1.0
TIMELINE_RULER_ITEM : `.${ClassName.TIMELINE_RULER_ITEM}`,
TIMELINE_RELATION_LINES : `.${ClassName.TIMELINE_RELATION_LINES}`,
TIMELINE_EVENTS : `.${ClassName.TIMELINE_EVENTS}`,
TIMELINE_SIDEBAR : `.${ClassName.TIMELINE_SIDEBAR}`,
TIMELINE_SIDEBAR_MARGIN : `.${ClassName.TIMELINE_SIDEBAR_MARGIN}`,// Added since v2.1.0
TIMELINE_SIDEBAR_ITEM : `.${ClassName.TIMELINE_SIDEBAR_ITEM}`,
TIMELINE_EVENT_NODE : `.${ClassName.TIMELINE_EVENT_NODE}`,
VIEWER_EVENT_TITLE : `.${ClassName.VIEWER_EVENT_TITLE}`,// Added since v2.1.0
VIEWER_EVENT_CONTENT : `.${ClassName.VIEWER_EVENT_CONTENT}`,// Added since v2.1.0
VIEWER_EVENT_META : `.${ClassName.VIEWER_EVENT_META}`,// Added since v2.1.0
VIEWER_EVENT_TYPE_POINTER : `.${ClassName.VIEWER_EVENT_TYPE_POINTER}`,
OVERLAY : `.${ClassName.OVERLAY}`,// Added since v2.1.0
PRESENT_TIME_MARKER : `.${ClassName.PRESENT_TIME_MARKER}`,// Added since v2.1.0
LOADER : `.${ClassName.LOADER_CONTAINER}`,
LOADER_ITEM : `.${ClassName.LOADER_ITEM}`,// Added since v2.1.0
DEFAULT_EVENTS : ".timeline-events"
}
const mapData = (() => {
const storeData = {}
let id = 1
return {
set( element, key, data ) {
if ( typeof element.key === 'undefined' ) {
element.key = { key, id }
id++
}
storeData[element.key.id] = data
},
get( element, key ) {
if ( ! element || typeof element.key === 'undefined' ) {
return null
}
const keyProperties = element.key
if ( keyProperties.key === key ) {
return storeData[keyProperties.id]
}
return null
},
delete( element, key ) {
if ( typeof element.key === 'undefined' ) {
return
}
const keyProperties = element.key
if ( keyProperties.key === key ) {
delete storeData[keyProperties.id]
delete element.key
}
}
}
})()
const Data = {
setData( instance, key, data ) {
mapData.set( instance, key, data )
},
getData( instance, key ) {
return mapData.get( instance, key )
},
removeData( instance, key ) {
mapData.delete( instance, key )
}
}
/* ----------------------------------------------------------------------------------------------------------------
* Plugin Core Class
* ----------------------------------------------------------------------------------------------------------------
*/
class Timeline {
constructor( element, config ) {
this._config = this._getConfig( config )
this._element = element
this._selector = null
this._isInitialized = false
this._isCached = false
this._isCompleted = false
this._isShown = false
this._isTouched = false
this._instanceProps = {}
this._observer = null // Added new since v2.0.0
this._beforeOpenEvent = null // Added new since v2.0.0
//this._countEventinCell = {} // since v2.0.0
Data.setData( element, DATA_KEY, this )
}
// Getters
static get VERSION() {
return VERSION
}
static get Default() {
return Default
}
// Private
/*
* @private: Define the default options of this plugin
*/
_getConfig( config ) {
// config = { ...Default, ...config } // Note: this is NG because two objects have not merged deeply
if ( config.startDatetime instanceof Date ) {
config.startDatetime = config.startDatetime.toString()
}
if ( config.endDatetime instanceof Date ) {
config.endDatetime = config.endDatetime.toString()
}
return this.mergeDeep( Default, config )
}
/*
* @private: Filter the given scale key name to allowed for plugin
*/
_filterScaleKeyName( key ) {
let filteredKey = null
switch( true ) {
case /^millenniums?|millennia$/i.test( key ):
filteredKey = 'millennium'
break
case /^century$/i.test( key ):
filteredKey = 'century'
break
case /^dec(ade|ennium)$/i.test( key ):
filteredKey = 'decade'
break
case /^lustrum$/i.test( key ):
filteredKey = 'lustrum'
break
case /^years?$/i.test( key ):
filteredKey = 'year'
break
case /^months?$/i.test( key ):
filteredKey = 'month'
break
case /^weeks?$/i.test( key ):
filteredKey = 'week'
break
case /^weekdays?$/i.test( key ):
filteredKey = 'weekday'
break
case /^da(y|te)s?$/i.test( key ):
filteredKey = 'day'
break
case /^hours?$/i.test( key ):
filteredKey = 'hour'
break
case /^quarter-?(|hour)$/i.test( key ):
filteredKey = 'quarterHour'
break
case /^half-?(|hour)$/i.test( key ):
filteredKey = 'halfHour'
break
case /^minutes?$/i.test( key ):
filteredKey = 'minute'
break
case /^seconds?$/i.test( key ):
filteredKey = 'second'
break
default:
filteredKey = 'millisecond'
}
return filteredKey
}
/*
* @private: Initialize the plugin
*/
async _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
}
this._calcVars()
this.showLoader()
if ( ! this._verifyMaxRenderableRange() ) {
throw new RangeError( `Timeline display period exceeds maximum renderable range.` )
}
this._renderView()
if ( ! this._isCached ) {
this._loadEvent()
}
if ( this._isCached ) {
await 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 )
)
$(_elem).on(
Event.TOUCHSTART_TIMELINE,
Selector.TIMELINE_MAIN,
( event ) => this._swipeStart( event )
)
$(_elem).on(
Event.TOUCHMOVE_TIMELINE,
Selector.TIMELINE_MAIN,
( event ) => this._swipeMove( event )
)
$(_elem).on(
Event.TOUCHEND_TIMELINE,
Selector.TIMELINE_MAIN,
( event ) => this._swipeEnd( 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 )
)
}
$(_elem).find( Selector.TIMELINE_CONTAINER ).on(
'scroll',
( event ) => this._scrollTimeline( event )
)
this._isCompleted = true
await this.hideLoader()
this.alignment()
if ( ! this._isInitialized ) {
const afterInitEvent = $.Event( Event.INITIALIZED, _elem )
$(_elem).trigger( afterInitEvent )
$(_elem).off( Event.INITIALIZED )
}
// Binding bs.popover
if ( $.fn['popover'] ) {
$('[data-toggle="popover"]').popover()
}
}
/*
* @private: Calculate each properties of the timeline instance
*/
_calcVars() {
let _opts = this._config,
_props = {},
_callback = ( _ac, _cv ) => {
if ( this.verifyScale( _cv ) ) {
_cv = this._filterScaleKeyName( _cv )
if ( _ac.indexOf( _cv ) === -1 ) {
_ac.push( _cv )
}
}
return _ac
}
_props.begin = this.supplement( null, this._getPluggableDatetime( _opts.startDatetime, 'first' ) ) // The milliseconds as start datetime (:> 開始日時のミリ秒
_props.end = this.supplement( null, this._getPluggableDatetime( _opts.endDatetime, 'last' ) ) // The milliseconds as end datetime (:> 終了日時のミリ秒
_props.scaleSize = this.supplement( null, _opts.minGridSize, this.validateNumeric ) // The minimum width of one grid on the base scale (:> 基本スケール上の1グリッドの最小幅
_props.rows = this._getPluggableRows() // The number of rows on the timeline container (:> タイムラインコンテナの行数
_props.rowSize = this.supplement( null, _opts.rowHeight, this.validateNumeric ) // The height of one row on the timeline container (:> タイムラインコンテナの1行の高さ
_props.width = this.supplement( null, _opts.width, this.validateNumeric ) // ?
_props.height = this.supplement( null, _opts.height, this.validateNumeric ) // ?
_props.isVLS = true // Whether is the variable length scale, fixed as constraint value of the "true" since v2.0.0b2
// _props.scale // The basic millisecond of one base grid on the setting scale (:> 設定スケールにおける起点グリッド1目盛の基準ミリ秒
// _props.grids // Number of base grids on the setting scale (= number of grids displayed) (:> 設定スケールにおける起点グリッド数(=表示されるグリッド数)
// _props.variableScale // An object that referable as the width of the base grid on the setting scale (:> 設定スケールにおける起点グリッド幅の幅員基準値となるオブジェクト
// _props.gridsMap // An object that mapped the available grids (:> 有効グリッドをマップしたオブジェクト -> これを定義するとメモリを食うのでNG
// _props.rulers // The available scale list on the ruler (:> 有効な目盛のスケールリスト
// _props.fullwidth // The total width of the timeline container (:> タイムラインコンテナの横幅の全長
// _props.fullheight // The total height of the timeline container (:> タイムラインコンテナの縦幅の全長
// _props.visibleWidth // The width of the timeline that will display (:> 表示されるタイムラインの横幅
// _props.visibleHeight // The height of the timeline that will display (:> 表示されるタイムラインの縦幅
_props.absX = 0 // An absolute X position on the page for using in swipe event (:> スワイプイベント用
_props.moveX = 0 // The moving position after doing the touchstart for using in swipe event (:> スワイプイベント用
// get all scales on ruler
let _rulers = _opts.ruler.top.lines.reduce( _callback, [])
if ( Object.hasOwnProperty.call( _opts.ruler, 'bottom' ) && Object.hasOwnProperty.call( _opts.ruler.bottom, 'lines' ) ) {
_rulers = [..._rulers, ..._opts.ruler.bottom.lines].reduce( _callback, [] )
}
if ( this.is_empty( _rulers ) ) {
_opts.ruler.top.lines.push( _opts.scale )
_rulers.push( _opts.scale )
}
_props.rulers = _rulers
this._instanceProps = _props // pre-cache
if ( _props.isVLS ) {
// For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
let _temp = this.verifyScale( _opts.scale, _props.begin, _props.end, _props.isVLS ),
_values = Object.values( _temp ),
//_averageVar = this.numRound( _values.reduce( ( a, v ) => a + v, 0 ) / _values.length, 4 ), // Average value within the range
//_minVar = Math.min( ..._values ),
_baseVar = /^weeks?$/i.test( _opts.scale ) ? Math.max( ..._values ) : Math.min( ..._values ),
_totalWidth = 0//,
//_baseToMin = true, // Whether use min value of scale as base grid width (:> スケールの最小値をベースグリッド幅として使用するか
//_baseVar = _baseToMin ? _minVar : _averageVar
switch ( true ) {
case /^millenniums?|millennia$/i.test( _opts.scale ):
case /^century$/i.test( _opts.scale ):
// unit: years = 365.25 * 24 * 60 * 60 * 1000
_props.scale = _baseVar * ( 365.25 * 24 * 60 * 60 * 1000 )
break
case /^dec(ade|ennium)$/i.test( _opts.scale ):
case /^lustrum$/i.test( _opts.scale ):
case /^years?$/i.test( _opts.scale ):
case /^months?$/i.test( _opts.scale ):
// unit: days = 24 * 60 * 60 * 1000
_props.scale = _baseVar * ( 24 * 60 * 60 * 1000 )
break
case /^weeks?$/i.test( _opts.scale ):
case /^(|week)days?$/i.test( _opts.scale ):
// unit: hours = 60 * 60 * 1000
_props.scale = _baseVar * ( 60 * 60 * 1000 )
break
case /^hours?$/i.test( _opts.scale ):
// unit: minutes = 60 * 1000
_props.scale = _baseVar * ( 60 * 1000 )
break
case /^minutes?$/i.test( _opts.scale ):
// unit: seconds = 1000
_props.scale = _baseVar * 1000
break
case /^seconds?$/i.test( _opts.scale ):
default:
// unit: milliseconds = 1
_props.scale = _baseVar * 1
break
}
//console.log( '!_calcVars::', _opts.scale, _temp, _values, _props )
_values.forEach( ( val ) => {
//console.log( `!!_calcVars::_totalWidth: ${_totalWidth} + ${this.numRound( ( val * _props.scaleSize ) / _baseVar, 2 )}` )
_totalWidth += this.numRound( ( val * _props.scaleSize ) / _baseVar, 2 )
})
_props.grids = _values.length
_props.variableScale = _temp
_props.fullwidth = _totalWidth
} else {
// Deprecated since v2.0.0 stable; in case of fixed length scale
}
_props.fullheight = _props.rows * _props.rowSize // Provisional value as theoretical value
// Define visible size according to full size of timeline (:> タイムラインのフルサイズに準じた可視サイズを定義
_props.visibleWidth = _props.width > 0 ? `${( _props.width <= _props.fullwidth ? _props.width : _props.fullwidth )}px` : 'max-content'
_props.visibleHeight = _props.height > 0 ? `${( _props.height <= _props.fullheight ? _props.height : _props.fullheight )}px` : 'max-content'
for ( let _prop in _props ) {
if ( /^(width|height|variableScale|absX|moveX)$/.test( _prop ) ) {
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.` )
}
//console.log( '!_calcVars::return:', _props )
this._instanceProps = _props
}
/*
* @private: Retrieve the pluggable datetime as milliseconds depend on specific preset keyword
*
* @param mixed key (required; preset keywords 'current', 'currently', 'auto' or seed of datetime)
* @param string round_type (optional; defaults to '')
*
* @return int (milliseconds as valid datetime)
*/
_getPluggableDatetime( key, round_type = '' ) {
let _opts = this._config,
_date = null,
getFirstDate = ( dateObj, scale ) => {
let _fullYear = dateObj.getFullYear(),
_remapYear = _fullYear >= 0 && Math.abs( _fullYear ) < 100 ? true : false,
_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( _fullYear, 0, 1 )
break
case /^months?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), 1 )
break
case /^(week|day)s?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate() )
break
case /^(|half|quarter)-?hours?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours() )
break
case /^minutes?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes() )
break
case /^seconds?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds() )
break
default:
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds(), dateObj.getMilliseconds() )
break
}
if ( _remapYear ) {
_tmpDate.setFullYear( _fullYear )
}
return _tmpDate
},
getLastDate = ( dateObj, scale ) => {
let _fullYear = dateObj.getFullYear(),
_remapYear = _fullYear >= 0 && Math.abs( _fullYear ) < 100 ? true : false,
_offset = _fullYear >= 0 ? -1 : 1,
_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( _fullYear + 1, 0, 1 )
_remapYear = ( _fullYear + 1 ) >= 0 && Math.abs( _fullYear + 1 ) < 100 ? true : false
_offset = ( _fullYear + 1 ) >= 0 ? -1 : 1
break
case /^months?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth() + 1, 1 )
break
case /^(week|day)s?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate() + 1 )
break
case /^(|half|quarter)-?hours?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours() + 1 )
break
case /^minutes?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes() + 1 )
break
case /^seconds?$/i.test( scale ):
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds() + 1 )
break
default:
_tmpDate = new Date( _fullYear, dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds(), dateObj.getMilliseconds() + 1 )
break
}
if ( _remapYear ) {
_tmpDate.setFullYear( _fullYear )
}
return new Date( _tmpDate.getTime() + _offset )
}
//console.log( '!_getPluggableDatetime::return:', key )
switch ( true ) {
case /^current(|ly)$/i.test( key ):
// now date
_date = new Date()
break
case /^auto$/i.test( key ): {
let _auto_range = _opts.range && _opts.range > 0 ? parseInt( _opts.range, 10 ) : 3,
_higherScale = /^(|week)days?$/i.test( _opts.scale ) ? 'month' : this.getHigherScale( _opts.scale )
if ( /^current(|ly)$/i.test( _opts.startDatetime ) ) {
_date = getFirstDate( new Date(), _opts.scale )
} else {
_date = this.getCorrectDatetime( _opts.startDatetime )
}
_date = this.modifyDate( _date, _auto_range, _higherScale )
break
}
default:
_date = this.getCorrectDatetime( key )
break
}
if ( ! this.is_empty( round_type ) ) {
if ( 'first' === round_type ) {
_date = getFirstDate( _date, _opts.scale )
} else
if ( 'last' === round_type ) {
_date = getLastDate( _date, _opts.scale )
}
}
//console.log( '!!_getPluggableDatetime::return:', _date )
return _date.getTime()
}
/*
* @private: Retrieve the pluggable parameter as an 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 ( Object.hasOwnProperty.call( params, 'extend' ) && typeof params.extend === 'string' ) {
params.extend = JSON.parse( JSON.stringify( ( new Function( `return ${params.extend}` ) )() ) )
}
} catch( e ) {
//console.warn( 'Can not parse to object therefor invalid param.' )
this._error( 'Can not parse to object therefor invalid param.', 'warn' )
}
}
return params
}
/*
* @private: Retrieve the pluggable rows of the timeline (:> プラガブルなタイムラインの行数を取得する
*/
_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
}
/*
* @private: Verify the display period of the timeline does not exceed the maximum renderable range (:> タイムラインの表示期間が最大描画可能範囲を超過していないか検証する
*/
_verifyMaxRenderableRange( scale = this._config.scale ) {
if ( this._config.disableLimitter ) {
this._debug( `The scale limiter has been OFF::${scale}: ${this._instanceProps.grids} / ${LimitScaleGrids[this._filterScaleKeyName( scale )]}` )
return true
} else {
this._debug( `Verify max renderable range::${scale}: ${this._instanceProps.grids} / ${LimitScaleGrids[this._filterScaleKeyName( scale )]}` )
return this._instanceProps.grids <= LimitScaleGrids[this._filterScaleKeyName( scale )]
}
}
/*
* @private: Render the view of timeline container
*/
_renderView() {
this._debug( '_renderView' )
let _elem = this._element,
_opts = this._config,
_props = this._instanceProps,
_tl_container = $('
', { class: ClassName.TIMELINE_CONTAINER }),// .jqtl-container
_tl_main = $('', { class: ClassName.TIMELINE_MAIN }),// .jqtl-main
$_el = $(_elem),// Cached an element
$_tl_parent = $_el.parent()
//console.log( '_renderView::', _elem, _opts, _props )
if ( $_el.length == 0 ) {
throw new TypeError( 'Does not exist the element to render a timeline container.' )
}
this._debug( `Timeline:{ fullWidth: ${_props.fullwidth}px, fullHeight: ${_props.fullheight}px, viewWidth: ${_props.visibleWidth}, viewHeight: ${_props.visibleHeight} }` )
$_el.css( 'position', 'relative' ) // initialize; not .empty()
if ( _opts.hideScrollbar ) {
_tl_container.addClass( ClassName.HIDE_SCROLLBAR )// .jqtl-hide-scrollbar
}
// Create the timeline headline (:> タイムラインの見出しを生成
$_el.prepend( this._createHeadline() )
// Create the timeline event container (:> タイムラインのイベントコンテナを生成
_tl_main.append( this._createEventContainer() )
// Create the timeline ruler (:> タイムラインの目盛を生成
if ( Object.hasOwnProperty.call( _opts.ruler, 'top' ) && Object.hasOwnProperty.call( _opts.ruler.top, 'lines' ) && ! this.is_empty( _opts.ruler.top.lines ) ) {
_tl_main.prepend( this._createRuler( 'top' ) )
}
if ( Object.hasOwnProperty.call( _opts.ruler, 'bottom' ) && Object.hasOwnProperty.call( _opts.ruler.bottom, 'lines' ) && ! this.is_empty( _opts.ruler.bottom.lines ) ) {
_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 )
$_el.append( _tl_container )
// Create the timeline footer (:> タイムラインのフッタを生成
$_el.append( this._createFooter() )
// Optimize the parent element of the timeline
if ( this.is_empty( $_el.attr( 'data-resized' ) ) ) {
//console.log( '_renderView::', $_tl_parent, Number(_tl_main.get(0).scrollWidth), Number($_el.get(0).scrollWidth), Number($_tl_parent.width() + 2) )
if ( $_el.get(0).scrollWidth > $_tl_parent.width() + 2 ) {
_tl_container.css({ width: $_tl_parent.width() - 2, height: _props.visibleHeight })
$_tl_parent.css({ maxWidth: '100vw', overflowX: 'hidden' })
} else {
_tl_container.css({ width: _props.visibleWidth, height: _props.visibleHeight })
}
$_el.attr( 'data-resized', true )
}
// Apply the theme color scheme
this.applyThemeStyle()
this._isShown = true
}
/*
* @private: Create the headline of the timeline (:> タイムラインの見出しを作成する
*/
_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 ),
_scale = _opts.scale,
_tl_headline = $('', { class: ClassName.TIMELINE_HEADLINE }),// .jqtl-headline
_wrapper = $('', { class: ClassName.TIMELINE_HEADLINE_WRAPPER })// .jqtl-headline-wrapper
if ( _title ) {
_wrapper.append( `${_opts.headline.title}
` )// .jqtl-timeline-title
}
if ( _range ) {
if ( _begin && _end ) {
let _meta = ''
if ( Object.hasOwnProperty.call( _format, 'custom' ) ) {
_scale = 'custom'
}
_meta = `${this.getLocaleString( _begin, _scale, _locale, _format )}${this.getLocaleString( _end, _scale, _locale, _format )}`
_wrapper.append( `${_meta}
` )// .jqtl-range-meta
}
}
if ( ! _display ) {
_tl_headline.addClass( ClassName.HIDE )// .jqtl-hide
}
return _tl_headline.append( _wrapper )
}
/*
* @private: Create the event container of the timeline (:> タイムラインのイベントコンテナを作成する
*/
_createEventContainer() {
let _opts = this._config,
_props = this._instanceProps,
_actualHeight = _props.fullheight + Math.ceil( _props.rows / 2 ) + 1,
_container = $('', { class: ClassName.TIMELINE_EVENT_CONTAINER, style: `height:${_actualHeight}px;` }),
_events_bg = $(``),
_events_lines = $(``),
_events_body = $('', { class: ClassName.TIMELINE_EVENTS }),
_cy = 0,
ctx_grid = _events_bg[0].getContext('2d'),
_grid_style = { horizontal: 'dotted', vertical: 'solid' },
drawRowRect = ( pos_y, color ) => {
color = this.supplement( _opts.colorScheme.theme.background, 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, style ) => {
let _correction = 0.5
switch ( true ) {
case /^solid$/i.test( style ):
style = 'solid'
break
case /^dotted$/i.test( style ):
style = 'dotted'
break
case /^none$/i.test( style ):
default:
return
}
ctx_grid.strokeStyle = this.hexToRgbA( _opts.colorScheme.theme.gridbase, 0.1 )// 'rgba( 51, 51, 51, 0.1 )'
ctx_grid.lineWidth = 1
ctx_grid.filter = 'url(#crisp)'
ctx_grid.beginPath()
if ( style === 'dotted' ) {
ctx_grid.setLineDash([ 1, 2 ])
} else {
ctx_grid.setLineDash([])
}
ctx_grid.moveTo( 0, pos_y + _correction )
ctx_grid.lineTo( _props.fullwidth, pos_y + _correction )
ctx_grid.closePath()
ctx_grid.stroke()
},
drawVerticalLine = ( pos_x, style ) => {
let _correction = -0.5
switch ( true ) {
case /^solid$/i.test( style ):
style = 'solid'
break
case /^dotted$/i.test( style ):
style = 'dotted'
break
case /^none$/i.test( style ):
default:
return
}
ctx_grid.strokeStyle = this.hexToRgbA( _opts.colorScheme.theme.gridbase, 0.1 )// 'rgba( 51, 51, 51, 0.025 )'
ctx_grid.lineWidth = 1
ctx_grid.filter = 'url(#crisp)'
ctx_grid.beginPath()
if ( style === 'dotted' ) {
ctx_grid.setLineDash([ 1, 2 ])
} else {
ctx_grid.setLineDash([])
}
ctx_grid.moveTo( pos_x + _correction, 0 )
ctx_grid.lineTo( pos_x + _correction, _actualHeight )
ctx_grid.closePath()
ctx_grid.stroke()
}
if ( Object.hasOwnProperty.call( _opts.effects, 'horizontalGridStyle' ) ) {
_grid_style.horizontal = _opts.effects.horizontalGridStyle
}
if ( Object.hasOwnProperty.call( _opts.effects, 'verticalGridStyle' ) ) {
_grid_style.vertical = _opts.effects.verticalGridStyle
}
_cy = 0
for ( let i = 0; i < _props.rows; i++ ) {
_cy += i % 2 == 0 ? 1 : 0
let _pos_y = ( i * _props.rowSize ) + _cy,
_color = this.hexToRgbA( _opts.colorScheme.theme.striped1, 0.1 )// '#FEFEFE'
if ( Object.hasOwnProperty.call( _opts.effects, 'stripedGridRow' ) && _opts.effects.stripedGridRow ) {
_color = i % 2 == 0 ? this.hexToRgbA( _opts.colorScheme.theme.striped1, 0.1 ) : this.hexToRgbA( _opts.colorScheme.theme.striped2, 0.25 )// '#FEFEFE' : '#F8F8F8'
}
drawRowRect( _pos_y, _color )
}
_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, _grid_style.horizontal )
}
if ( _props.isVLS ) {
// For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
let _sy = 0,
_baseVar
switch (true) {
case /^millenniums?|millennia$/i.test( _opts.scale ):
case /^century$/i.test( _opts.scale ):
// unit: years = 365.25 * 24 * 60 * 60 * 1000
_baseVar = _props.scale / ( 365.25 * 24 * 60 * 60 * 1000 )
break
case /^dec(ade|ennium)$/i.test( _opts.scale ):
case /^lustrum$/i.test( _opts.scale ):
case /^years?$/i.test( _opts.scale ):
case /^months?$/i.test( _opts.scale ):
// unit: days = 24 * 60 * 60 * 1000
_baseVar = _props.scale / ( 24 * 60 * 60 * 1000 )
break
case /^weeks?$/i.test( _opts.scale ):
case /^(|week)days?$/i.test( _opts.scale ):
// unit: hours = 60 * 60 * 1000
_baseVar = _props.scale / ( 60 * 60 * 1000 )
break
case /^hours?$/i.test( _opts.scale ):
// unit: minutes = 60 * 1000
_baseVar = _props.scale / ( 60 * 1000 )
break
case /^minutes?$/i.test( _opts.scale ):
// unit: seconds = 1000
_baseVar = _props.scale / 1000
break
case /^seconds?$/i.test( _opts.scale ):
default:
// unit: milliseconds = 1
_baseVar = _props.scale / 1
break
}
for ( let _key of Object.keys( _props.variableScale ) ) {
_sy += this.numRound( ( _props.variableScale[_key] * _props.scaleSize ) / _baseVar, 2 )
drawVerticalLine( _sy, _grid_style.vertical )
}
} else {
// In case of fixed length scale; Deprecated
for ( let i = 1; i < _props.grids; i++ ) {
drawVerticalLine( ( i * _props.scaleSize ), false )
}
}
return _container.append( _events_bg ).append( _events_lines ).append( _events_body )
}
/*
* @private: Create the ruler of the timeline (:> タイムラインの目盛を作成する
*/
_createRuler( position ) {
let _opts = this._config,
_props = this._instanceProps,
ruler_line = this.supplement( [ _opts.scale ], _opts.ruler[position].lines, ( def, val ) => {
if ( Array.isArray( val ) && val.length > 0 ) {
if ( Object.hasOwnProperty.call( _opts.ruler, 'truncateLowers' ) && _opts.ruler.truncateLowers ) {
let _ignore_scales = this.findScale( _opts.scale, 'lower all' ),
_filter_scales = val.filter( ( scl ) => ! _ignore_scales.includes( this._filterScaleKeyName( scl ) ) )
//console.log( '!_createRuler::truncateLowers:', _opts.scale, _ignore_scales, val, _filter_scales )
val = _filter_scales
}
return val
} else {
return 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 = $('', { class: `${PREFIX}ruler-${position}`, style: `height:${_fullheight}px;` }),
_ruler_bg = $(``),
_ruler_body = $('', { class: `${PREFIX}ruler-content-${position}` }),
_finalLines = 0,
ctx_ruler = _ruler_bg[0].getContext('2d')
// Override ruler options for applying theme; added since v2.1.0
if ( 'inherit' === ruler_opts.color ) {
ruler_opts.color = _opts.colorScheme.theme.subtext
}
if ( 'inherit' === ruler_opts.background ) {
ruler_opts.background = _opts.colorScheme.theme.background
}
//console.log( '!_createRuler:', ruler_line, ruler_opts )
// Draw background of ruler
ctx_ruler.fillStyle = ruler_opts.background
ctx_ruler.fillRect( 0, 0, ctx_ruler.canvas.width, ctx_ruler.canvas.height )
// Draw stroke of ruler
ctx_ruler.strokeStyle = this.hexToRgbA( _opts.colorScheme.theme.gridbase, 0.1 )// '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 = -0.5
// 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 )
}
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 )
}
/*
* @private: Filter to aggregate the grid width of the variable length scale (:> 可変長スケールのグリッド幅を集約するフィルタ
*
* @param: target_scale = a scale of one line on the ruler (:> 目盛1行分のスケール
* @return: An object of actual grid widths for each individual scale on the set scale of the timeline container (:> タイムラインコンテナの指定スケール上における各個別スケールの実際のグリッド幅のオブジェクト
*/
_filterVariableScale( target_scale ) {
let _opts = this._config,
_props = this._instanceProps,
// _opts.scale が day の場合は時間/日、それ以外の場合は日/年となる
scales = _props.variableScale,
retObj = {},
_baseVar
switch (true) {
case /^millenniums?|millennia$/i.test( _opts.scale ):
case /^century$/i.test( _opts.scale ):
// unit: years = 365.25 * 24 * 60 * 60 * 1000
_baseVar = _props.scale / ( 365.25 * 24 * 60 * 60 * 1000 )
break
case /^dec(ade|ennium)$/i.test( _opts.scale ):
case /^lustrum$/i.test( _opts.scale ):
case /^years?$/i.test( _opts.scale ):
case /^months?$/i.test( _opts.scale ):
// unit: days = 24 * 60 * 60 * 1000
_baseVar = _props.scale / ( 24 * 60 * 60 * 1000 )
break
case /^weeks?$/i.test( _opts.scale ):
case /^(|week)days?$/i.test( _opts.scale ):
// unit: hours = 60 * 60 * 1000
_baseVar = _props.scale / ( 60 * 60 * 1000 )
break
case /^hours?$/i.test( _opts.scale ):
// unit: minutes = 60 * 1000
_baseVar = _props.scale / ( 60 * 1000 )
break
case /^minutes?$/i.test( _opts.scale ):
// unit: seconds = 1000
_baseVar = _props.scale / 1000
break
case /^seconds?$/i.test( _opts.scale ):
default:
// unit: milliseconds = 1
_baseVar = _props.scale / 1
break
}
// グリッド幅の起点となるスケールは _opts.scale で、表示用にフィルタされるスケールは target_scale なので、それに合わせてサイズを計算する
//console.log( `!_filterVariableScale::${_opts.scale} -> ${target_scale}:`, scales )
for ( let _dt of Object.keys( scales ) ) {
let grid_size = this.numRound( ( scales[_dt] * _props.scaleSize ) / _baseVar, 2 ),
_newKey = null,
_arr = _dt.split(','),
_tmpDt = /^weeks?$/i.test( _opts.scale ) ? this.getFirstDayOfWeek( parseInt( _arr[1], 10 ), parseInt( _arr[0], 10 ) ) : this.getCorrectDatetime( _arr[0] ),
_temp
//console.log( '!!_filterVariableScale:', _dt, this.getCorrectDatetime( _dt ), _props.scaleSize, scales[_dt], grid_size )
switch ( true ) {
case /^millenniums?|millennia$/i.test( target_scale ):
//_years = 1000
_newKey = Math.ceil( _tmpDt.getFullYear() / 1000 )
//grid_size = this.numRound( ( _years * _props.scaleSize ) / _baseVar, 2 )
break
case /^century$/i.test( target_scale ):
//_years = 100
_newKey = Math.ceil( _tmpDt.getFullYear() / 100 )
//grid_size = this.numRound( ( _years * _props.scaleSize ) / _baseVar, 2 )
break
case /^dec(ade|ennium)$/i.test( target_scale ):
//_years = 10
_newKey = Math.ceil( _tmpDt.getFullYear() / 10 )
//grid_size = this.numRound( ( _years * _props.scaleSize ) / _baseVar, 2 )
break
case /^lustrum$/i.test( target_scale ):
//_years = 5
_newKey = Math.ceil( _tmpDt.getFullYear() / 5 )
//grid_size = this.numRound( ( _years * _props.scaleSize ) / _baseVar, 2 )
break
case /^years?$/i.test( target_scale ):
//_days = scales[_dt]
_newKey = `${_tmpDt.getFullYear()}`
//grid_size = this.numRound( ( _days * _props.scaleSize ) / _baseVar, 2 )
break
case /^months?$/i.test( target_scale ):
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}`
break
case /^weeks?$/i.test( target_scale ):
if ( /^weeks?$/i.test( _opts.scale ) ) {
_newKey = _arr.join()
} else {
_temp = this.getWeek( _tmpDt )
_newKey = `${_tmpDt.getFullYear()},${_temp}`
}
//console.log( `!!_filterVariableScale::${target_scale}:`, _opts.scale, _arr, _tmpDt, _temp, _newKey )
break
case /^weekdays?$/i.test( target_scale ):
if ( /^weeks?$/i.test( _opts.scale ) && this.is_empty( retObj ) ) {
_tmpDt = new Date( _props.begin )
}
_temp = _tmpDt.getDay()
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}/${_tmpDt.getDate()},${_temp}`
break
case /^days?$/i.test( target_scale ):
if ( /^weeks?$/i.test( _opts.scale ) && this.is_empty( retObj ) ) {
_tmpDt = new Date( _props.begin )
}
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}/${_tmpDt.getDate()}`
break
case /^hours?$/i.test( target_scale ):
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}/${_tmpDt.getDate()} ${_tmpDt.getHours()}`
//retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0`] = grid_size
break
case /^minutes?$/i.test( target_scale ):
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}/${_tmpDt.getDate()} ${_tmpDt.getHours()}:${_tmpDt.getMinutes()}`
//retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0:00`] = grid_size
break
case /^seconds?$/i.test( target_scale ):
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}/${_tmpDt.getDate()} ${_tmpDt.getHours()}:${_tmpDt.getMinutes()}:${_tmpDt.getSeconds()}`
//retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0:00:00`] = grid_size
break
default:
_newKey = `${_tmpDt.getFullYear()}/${_tmpDt.getMonth() + 1}/${_tmpDt.getDate()} ${_tmpDt.getHours()}:${_tmpDt.getMinutes()}:${_tmpDt.getSeconds()}.${_tmpDt.getMilliseconds()}`
//retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}`] = grid_size
break
}
//console.log( `!!!_filterVariableScale::${target_scale}:`, _dt, _newKey, grid_size )
if ( Object.hasOwnProperty.call( retObj, _newKey ) ) {
retObj[_newKey] += grid_size
} else {
retObj[_newKey] = grid_size
}
}
//console.log( `!!!_filterVariableScale::${_opts.scale} -> ${target_scale}:`, retObj )
return retObj
}
/*
* @private: Create the content of ruler of the timeline (:> タイムラインの目盛本文を作成する
*/
_createRulerContent( _line_grids, line_scale, ruler ) {
let 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 = $('', { class: ClassName.TIMELINE_RULER_LINES, style: `width:100%;height:${line_height}px;` })
//console.log( '!_createRulerContent::', ruler, line_scale, text_color, locale, format )
for ( let _key of Object.keys( _line_grids ) ) {
let _item_width = _line_grids[_key],
_line = $('', { 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, this._filterScaleKeyName( line_scale ), locale, format ),
_data_ruler_item = ''
//console.log( '!_createRulerContent::', _key, line_scale, _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.toString().length, this.strWidth( _ruler_string ), $(this._element).get(0).clientWidth )
if ( _item_width > $(this._element).width() ) {
_line.children('span').addClass( ClassName.RULER_ITEM_ALIGN_LEFT )
}
*/
}
_ruler_lines.append( _line ).attr( 'data-ruler-scope', line_scale )
}
return _ruler_lines
}
/*
* @private: Create the side indexes of the timeline (:> タイムラインのサイド・インデックスを作成する
*/
_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 = $('', { class: ClassName.TIMELINE_SIDEBAR }),
_margin = $('', { class: ClassName.TIMELINE_SIDEBAR_MARGIN }),
_list = $('', { class: ClassName.TIMELINE_SIDEBAR_ITEM }),
_item_h = this.numRound( (_props.fullheight + Math.ceil(_props.rows / 2)) / _props.rows, 2 ), // Actual height of container: fullheight + Math.ceil( rows / 2 )
_c = -0.5
if ( _sticky ) {
_wrapper.addClass( ClassName.STICKY_LEFT )
}
if ( _overlay ) {
_list.addClass( ClassName.OVERLAY )
}
if ( margin.top > 0 ) {
_wrapper.prepend( _margin.clone().css( 'height', `${margin.top}px` ) )
}
for ( let i = 0; i < _props.rows; i++ ) {
let _item = _list.clone().html( `${_sbList[i]}` )
if ( i + 1 == _props.rows ) {
_item.css( 'height', `${(_item_h + _c)}px` ).css( 'line-height', `${(_item_h + _c)}px` )
} else {
_item.css( 'height', `${(_item_h - 1)}px` ).css( 'line-height', `${(_item_h - 1)}px` )
}
_wrapper.append( _item )
}
if ( margin.bottom > 0 ) {
_wrapper.append( _margin.clone().css( 'height', `${( margin.bottom + _c )}px` ) )
}
return _wrapper
}
/*
* @private: Create the footer of the timeline (:> タイムラインのフッターを作成する
*/
_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 ),
_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 ),
_scale = _opts.scale,
_tl_footer = $('', { class: ClassName.TIMELINE_FOOTER })
if ( _range ) {
if ( _begin && _end ) {
if ( Object.hasOwnProperty.call( _format, 'custom' ) ) {
_scale = 'custom'
}
let _meta = `${this.getLocaleString( _begin, _scale, _locale, _format )}${this.getLocaleString( _end, _scale, _locale, _format )}`
_tl_footer.append( `${_meta}
` )
}
}
if ( _content ) {
_tl_footer.append( `` )
}
if ( ! _display ) {
_tl_footer.addClass( ClassName.HIDE )
}
return _tl_footer
}
/*
* @private: Load all enabled events markupped on target element to the timeline object (:> 対象要素にマークアップされたすべての有効なイベントをタイムラインにロードする
* Firstly load default events bound to plugin config (:> 最初にプラグイン設定にバインドされた初期イベントをロードする
*/
_loadEvent() {
this._debug( '_loadEvent' )
let _that = this,
_elem = this._element,
_opts = this._config,
_default_events = _opts.eventData,
_event_list = $(_elem).find( Selector.DEFAULT_EVENTS ),
_cnt = _default_events.length,
events = [],
lastEventId = 0
_event_list.children().each(function() {
let _attr = $(this).attr( 'data-timeline-node' )
if ( typeof _attr !== 'undefined' && _attr !== false ) {
_cnt++
}
})
if ( _cnt == 0 ) {
//this._debug( 'Enable event does not exist.' )
//console.warn( 'Enable event does not exist.' )
this._error( 'Enable event does not exist.', 'warn' )
}
// Register Event Data
if ( _default_events.length > 0 ) {
_default_events.forEach(( _evt_obj ) => {
let _one_event = {}
if ( ! this.is_empty( _evt_obj ) ) {
_one_event = this._registerEventData( '', _evt_obj )
events.push( _one_event )
lastEventId = Math.max( lastEventId, parseInt( _one_event.eventId, 10 ) )
}
})
}
_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
let cacheIds = [] // for checking duplication of 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 )
})
// Hook to event colors; Added instead of merging setColorEvent of PR#37
events.forEach( ( _evt, _i, _this ) => {
if ( Object.hasOwnProperty.call( _opts.colorScheme, 'event' ) && typeof _opts.colorScheme.event === 'object' ) {
// Firstly overwrite default colors
if ( Object.hasOwnProperty.call( _opts.colorScheme.event, 'text' ) && _evt.color === Default.colorScheme.event.text && Default.colorScheme.event.text !== _opts.colorScheme.event.text ) {
_this[_i].color = _opts.colorScheme.event.text
}
if ( Object.hasOwnProperty.call( _opts.colorScheme.event, 'background' ) && _evt.bgColor === Default.colorScheme.event.background && Default.colorScheme.event.background !== _opts.colorScheme.event.background ) {
_this[_i].bgColor = _opts.colorScheme.event.background
}
if ( Object.hasOwnProperty.call( _opts.colorScheme.event, 'border' ) && _evt.bdColor === Default.colorScheme.event.border && Default.colorScheme.event.border !== _opts.colorScheme.event.border ) {
_this[_i].bdColor = _opts.colorScheme.event.border
}
}
if ( Object.hasOwnProperty.call( _opts.colorScheme, 'hookEventColors' ) && typeof _opts.colorScheme.hookEventColors === 'function' ) {
// Lastly, overwrite current colors
let _new_colors = _opts.colorScheme.hookEventColors( _evt, { text: _this[_i].color, border: _this[_i].bdColor, background: _this[_i].bgColor } ) || undefined
if ( typeof _new_colors === 'object' ) {
if ( Object.hasOwnProperty.call( _new_colors, 'text' ) && _evt.color !== _new_colors.text ) {
_this[_i].color = _new_colors.text
}
if ( Object.hasOwnProperty.call( _new_colors, 'background' ) && _evt.bgColor !== _new_colors.background ) {
_this[_i].bgColor = _new_colors.background
}
if ( Object.hasOwnProperty.call( _new_colors, 'border' ) && _evt.bdColor !== _new_colors.border ) {
_this[_i].bdColor = _new_colors.border
}
}
}
})
this._isCached = this._saveToCache( events )
}
/*
* @private: Register one event data as object (:> イベントデータをオブジェクトとして登録する
*/
_registerEventData( event_element, params ) {
let _opts = this._config,
_props = this._instanceProps,
new_event = {
...EventParams,
...{
uid : this.generateUniqueID(),
label : $(event_element).html()
}
},
_relation = {},
_x, _w, _row, _c //, _pointSize
//console.log( '!_registerEventData:', EventParams, new_event )
if ( Object.hasOwnProperty.call( params, 'start' ) && ! this.is_empty( params.start ) ) {
_x = this._getCoordinateX( params.start )
new_event.x = this.numRound( _x, 2 )
if ( Object.hasOwnProperty.call( params, '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 ) && ! Object.hasOwnProperty.call( params, '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
}
_row = Object.hasOwnProperty.call( params, 'row' ) ? parseInt( params.row, 10 ) : 1
_c = Math.floor( _row / 2 )
new_event.y = ( _row - 1 ) * _opts.rowHeight + new_event.margin + _c
new_event.height = _opts.rowHeight - (_opts.marginHeight * 2)
Object.keys( new_event ).forEach( ( _prop ) => {
switch( true ) {
case /^eventId$/i.test( _prop ):
if ( Object.hasOwnProperty.call( params, 'id' ) ) {
new_event.eventId = parseInt( params.id, 10 )
} else {
new_event.eventId = parseInt( params[_prop], 10 ) || 0
}
break
case /^(label|content)$/i.test( _prop ):
if ( Object.hasOwnProperty.call( params, _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()
}
break
case /^relation$/i.test( _prop ):
// For drawing the relation line
if ( /^mix(|ed)$/i.test( _opts.type ) || /^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 ) + ( ( ( params.row || 1 ) - 1 ) * 0.5 )
//console.log( '!_registerEventData:', params, _props, new_event.x, new_event.y, _pointSize, _relation )
new_event[_prop] = {
...params[_prop],
..._relation
}
}
break
default:
if ( Object.hasOwnProperty.call( params, _prop ) && ! this.is_empty( params[_prop] ) ) {
new_event[_prop] = params[_prop]
}
break
}
})
}
//console.log( '!_registerEventData:', new_event )
return new_event
}
/*
* @private: Get the coordinate X on the timeline of any date
*/
_getCoordinateX( date ) {
/*
// add new since v2.0.0 : start
if ( this._config.scale === "day" ) {
let dateAdjust = new Date( date )
if ( dateAdjust.getHours() <= this._config.startHour ) {
date = `${dateAdjust.getFullYear()}-${(dateAdjust.getMonth() + 1)}-${dateAdjust.getDate()} 00:00:00`
} else
if ( dateAdjust.getHours() >= this._config.endHour ) {
date = `${dateAdjust.getFullYear()}-${(dateAdjust.getMonth() + 1)}-${dateAdjust.getDate()} 23:59:59`
}
}
// add new since v2.0.0 : end
*/
let _props = this._instanceProps,
_date = this.supplement( null, this._getPluggableDatetime( date ) ),
coordinate_x = 0
//console.log( '!_getCoordinateX::', _props, _date )
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.' )
this._error( 'Cannot parse date because invalid format or undefined.', 'warn' )
}
return coordinate_x
}
/*
* @private: Cache the event data to the web storage
*/
_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.` )
}
}
/*
* @private: Load the cached event data from the web storage
*/
_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
}
/*
* @private: Remove the cache data on the web storage
*/
_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.` )
}
}
/*
* @private: Controller method to place event data on timeline
*/
_placeEvent() {
return new Promise(( resolve, reject ) => {
this._debug( '_placeEvent' )
if ( ! this._isCached ) {
//return
reject('No Cached Event')
}
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(),
placedEvents = []
// c.f. https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
this._observer = new MutationObserver(( mutations ) => {
mutations.forEach((mutation) => {
let _self = mutation.target
switch( mutation.type ) {
case 'childList':
// console.log( 'MutationObserver::childList:', mutation.addedNodes.length, placedEvents.length )
if ( mutation.addedNodes.length == placedEvents.length ) {
_relation_lines.attr( 'data-state', 'show' )
_evt_container.attr( 'data-state', 'show' )
}
break;
case 'attributes':
if ( mutation.attributeName === 'data-state' ) {
// console.log( 'MutationObserver::attributes:', $(_self).attr('data-state') )
if ( $(_self).attr('data-state') === 'shown' ) {
resolve('Completed Placing')
} else
if ( $(_self).attr('data-state') === 'show' ) {
setTimeout(() => {
_relation_lines.attr( 'data-state', 'shown' )
_evt_container.attr( 'data-state', 'shown' )
}, 300)
}
}
break;
}
})
})
this._observer.observe( _evt_container.get(0), { childList: true, attributes: true, subtree: true, attributeOldValue: true } )
if ( events.length > 0 ) {
_evt_container.empty()
/*
// add new since v2.0.0 : start
events = events.sort( (a, b) => a.width < b.width ? 1 : -1 ) // sort elements
// add new since v2.0.0 : end
*/
events.forEach( ( _evt ) => {
// Apply color scheme to the creation event
if ( _evt.color === Default.colorScheme.event.text && Default.colorScheme.event.text !== _opts.colorScheme.event.text ) {
_evt.color = _opts.colorScheme.event.text
}
if ( _evt.bgColor === Default.colorScheme.event.background && Default.colorScheme.event.background !== _opts.colorScheme.event.background ) {
_evt.bgColor = _opts.colorScheme.event.background
}
if ( _evt.bdColor === Default.colorScheme.event.border && Default.colorScheme.event.border !== _opts.colorScheme.event.border ) {
_evt.bdColor = _opts.colorScheme.event.border
}
let _evt_elem = this._createEventNode( _evt )
if ( _evt_elem ) {
//_evt_container.append( _evt_elem )
placedEvents.push( _evt_elem )
}
})
if ( placedEvents.length > 0 ) {
_evt_container.append( ...placedEvents )
}
} else {
_relation_lines.attr( 'data-state', 'show' )
_evt_container.attr( 'data-state', 'show' )
}
if ( /^mix(|ed)$/i.test( _opts.type ) || /^point(|er)$/i.test( _opts.type ) ) {
this._drawRelationLine( events )
}
if ( Object.hasOwnProperty.call( _opts.effects, 'presentTime' ) && _opts.effects.presentTime ) {
this._viewPresentTime()
}
resolve(true)
})// return Promise
}
/*
* @private: Create an event element on the timeline (:> タイムライン上にイベント要素を作成する
*/
_createEventNode( params ) {
let _opts = this._config,
_props = this._instanceProps,
_evt_elem = $('', {
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 : `${params.label}
`
}),
_is_bar = true
// Whether this event type is bar or point
if ( /^point(|er)$/i.test( _opts.type ) ) {
_is_bar = false // point type
} else
if ( /^mix(|ed)$/i.test( _opts.type ) ) {
if ( /^point(|er)$/i.test( params.type ) ) {
_is_bar = false // point type
} else
if ( params.width < 1 ) {
_is_bar = ! /^bar$/i.test( params.type ) ? false : true
}
}
//console.log( '!_createEventNode:', params, _is_bar )
// 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
// add new since v2.0.0 : start
//_evt_elem.append( `` )
// add new since v2.0.0 : end
}
} 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 ( ! _is_bar ) {
// 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 )
// add new since v2.0.0 : start
//_evt_elem.prepend( `` )
// add new since v2.0.0 : end
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
// add new since v2.0.0 : start
//_evt_elem.append( `` )
//_evt_elem.prepend( `` )
// add new since v2.0.0 : end
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 ( ! _is_bar ) {
// If this event is the point type
if ( params.width < 0 ) {
return null
}
let _pointSize = this._getPointerSize( params.size, params.margin ),
_shiftX = this.numRound( params.x - ( _pointSize / 2 ), 2 ) - params.margin,
_shiftY = this.numRound( params.y + ( ( params.height - _pointSize ) / 2 ), 2 ) - params.margin
//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 this event is the bar type
if ( params.width < 1 ) {
return null
}
_evt_elem.css( 'left', `${params.x}px` ).css( 'width', `${params.width}px` )
/*
// add new since v2.0.0 : start
if ( params.width < 15 ) {
// Create Event info on bullet point
let date_start = new Date( params.start ),
date_start_grid, correction_x, correction_y
switch ( true ) {
case /^months?$/i.test( _opts.scale ):
correction_x = 6
date_start_grid = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-1`
_evt_elem.html( ` ${date_start.getDate()} : ${params.label}
` )
break
case /^(|week)days?$/i.test( _opts.scale ):
correction_x = 0
date_start_grid = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-${date_start.getDate()} 00:00`
_evt_elem.html( ` ${date_start.getHours()}:${date_start.getMinutes()} : ${params.label}
` )
break
case /^hours?$/i.test( _opts.scale ):
correction_x = 0
date_start_grid = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-${date_start.getDate()} ${date_start.getHours()}:00`
_evt_elem.html( ` ${date_start.getHours()}:${date_start.getMinutes()} : ${params.label}
` )
break
}
if ( this._countEventinCell[params.row] == null ) {
this._countEventinCell[params.row] = {}
}
if ( this._countEventinCell[params.row][date_start_grid] == null ) {
this._countEventinCell[params.row][date_start_grid] = 0
}
correction_y = this._countEventinCell[params.row][date_start_grid] * EventParams.height
this._countEventinCell[params.row][date_start_grid]++
if ( (this._countEventinCell[params.row][date_start_grid] * EventParams.height) > this._config.rowHeight ) {
this._config.rowHeight = this._countEventinCell[params.row][date_start_grid] * EventParams.height
this.reload( this._config )
//console.log("Reload : " + this._config.rowHeight);
}
params.x = this._getCoordinateX( date_start_grid )
_evt_elem.css( 'top', `${this.numRound( params.y+correction_y, 2 )}px` ).css( 'backgroundColor', 'transparent' )
.css( 'color', 'black' ).css( 'left', `${this.numRound( params.x+correction_x, 2 )}px` )
.css( 'width', `${this._config.minGridSize}px` ) // .css('height', `12px`)
//return null
} else {
let date_start = new Date( params.start ),
date_end = new Date( params.end ),
date_test_grid, correction_y, date_test_grid_index
switch ( true ) {
case /^months?$/i.test( _opts.scale ):
date_test_grid_index = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-1`
break
case /^(|week)days?$/i.test( _opts.scale ):
date_test_grid_index = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-${date_start.getDate()} 00:00`
break
case /^hours?$/i.test( _opts.scale ):
date_test_grid_index = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-${date_start.getDate()} ${date_start.getHours()}:00`
break
}
if ( this._countEventinCell[params.row] == null ) {
this._countEventinCell[params.row] = {}
}
if ( this._countEventinCell[params.row][date_test_grid_index] == null ) {
this._countEventinCell[params.row][date_test_grid_index] = 0
}
correction_y = this._countEventinCell[params.row][date_test_grid_index]
// For all grid between start / end, search max Position Y
date_test_grid = date_start
while ( date_test_grid <= date_end ) {
switch ( true ) {
case /^months?$/i.test( _opts.scale ):
date_test_grid = new Date( date_test_grid.getFullYear(), date_test_grid.getMonth() + 1, 1 )
date_test_grid_index = `${date_test_grid.getFullYear()}-${(date_test_grid.getMonth() + 1)}-1`
break;
case /^(|week)days?$/i.test( _opts.scale ):
date_test_grid = new Date( date_test_grid.getFullYear(), date_test_grid.getMonth(), date_test_grid.getDate() + 1 )
date_test_grid_index = `${date_test_grid.getFullYear()}-${(date_test_grid.getMonth() + 1)}-${date_test_grid.getDate()} 00:00`
break;
case /^hours?$/i.test( _opts.scale ):
date_test_grid = new Date( date_test_grid.getFullYear(), date_test_grid.getMonth(), date_test_grid.getDate(), date_test_grid.getHours() + 1 )
date_test_grid_index = `${date_test_grid.getFullYear()}-${(date_test_grid.getMonth() + 1)}-${date_test_grid.getDate()} ${date_test_grid.getHours()}:00`
break;
}
if ( this._countEventinCell[params.row] == null ) {
this._countEventinCell[params.row] = {}
}
if ( this._countEventinCell[params.row][date_test_grid_index] == null ) {
this._countEventinCell[params.row][date_test_grid_index] = 0
}
correction_y = Math.max( this._countEventinCell[params.row][date_test_grid_index], correction_y )
}
// set new position
correction_y++
switch ( true ) {
case /^months?$/i.test( _opts.scale ):
date_test_grid_index = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-1`
break;
case /^(|week)days?$/i.test( _opts.scale ):
date_test_grid_index = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-${date_start.getDate()} 00:00`
break;
case /^hours?$/i.test( _opts.scale ):
date_test_grid_index = `${date_start.getFullYear()}-${(date_start.getMonth() + 1)}-${date_start.getDate()} ${date_start.getHours()}:00`
break;
}
this._countEventinCell[params.row][date_test_grid_index] = correction_y;
// For all grid between start / end, set new Position Y
date_test_grid = date_start;
while ( date_test_grid <= date_end ) {
switch ( true ) {
case /^months?$/i.test( _opts.scale ):
date_test_grid = new Date( date_test_grid.getFullYear(), date_test_grid.getMonth() + 1, 1 )
date_test_grid_index = `${date_test_grid.getFullYear()}-${(date_test_grid.getMonth() + 1)}-1`
break
case /^(|week)days?$/i.test( _opts.scale ):
date_test_grid = new Date( date_test_grid.getFullYear(), date_test_grid.getMonth(), date_test_grid.getDate() + 1 )
date_test_grid_index = `${date_test_grid.getFullYear()}-${(date_test_grid.getMonth() + 1)}-${date_test_grid.getDate()} 00:00`
break
case /^hours?$/i.test( _opts.scale ):
date_test_grid = new Date( date_test_grid.getFullYear(), date_test_grid.getMonth(), date_test_grid.getDate(), date_test_grid.getHours() + 1 )
date_test_grid_index = `${date_test_grid.getFullYear()}-${(date_test_grid.getMonth() + 1)}-${date_test_grid.getDate()} ${date_test_grid.getHours()}:00`
break
}
if ( this._countEventinCell[params.row] == null ) {
this._countEventinCell[params.row] = {}
}
if ( this._countEventinCell[params.row][date_test_grid_index] == null ) {
this._countEventinCell[params.row][date_test_grid_index] = 0
}
this._countEventinCell[params.row][date_test_grid_index] = correction_y
}
if ( ( correction_y * EventParams.height ) > this._config.rowHeight ) {
this._config.rowHeight = correction_y * EventParams.height
this.reload( this._config )
}
_evt_elem.css( 'top', `${this.numRound( params.y + ( (correction_y - 1) * EventParams.height ), 2 )}px` )
.css( 'left', `${params.x}px` ).css( 'width', `${params.width}px` ).css( 'border', `1px solid ${EventParams.bdColor}` )
}
// add new since v2.0.0 : end
*/
}
_evt_elem.attr( 'data-uid', params.uid )
/*
// add new since v2.0.0 : start
_evt_elem.attr( 'data-category', params.category);
// add new since v2.0.0 : end
*/
if ( ! this.is_empty( params.image ) ) {
if ( ! _is_bar ) {
_evt_elem.css( 'background-image', `url(${params.image})` )
} else {
let _imgSize = params.height - ( params.margin * 2 )
_evt_elem.prepend( `` )
}
}
if ( _is_bar && _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 ( ! Object.hasOwnProperty.call( params.extend, 'content' ) ) {
_evt_elem.attr( 'data-content', params.content )
}
}
}
}
if ( ! this.is_empty( params.callback ) ) {
_evt_elem.attr( 'data-callback', params.callback )
}
return _evt_elem
}
/*
* @private: Retrieve the diameter size (pixel) of pointer (:> ポインタの直径サイズ(ピクセル値)を取得する
*/
_getPointerSize( key, margin ) {
let _props = this._instanceProps,
_max = Math.min( (_props.scaleSize - ( margin * 2 )), (_props.rowSize - ( margin * 2 )) ),
_size = null
switch ( true ) {
case /^([1-9]\d*|0)$/i.test( key ):
_size = Math.max( parseInt( key, 10 ), MIN_POINTER_SIZE )
break
case /^small$/i.test( key ):
_size = Math.max( this.numRound( _max / 4, 2 ), MIN_POINTER_SIZE )
break
case /^large$/i.test( key ):
_size = Math.max( this.numRound( _max * 0.75, 2 ), MIN_POINTER_SIZE )
break
case /^normal$/i.test( key ):
default:
_size = Math.max( this.numRound( _max / 2, 2 ), MIN_POINTER_SIZE )
break
}
// console.log( '!_getPointerSize:', _props, key, _max, _size )
return _size
}
/*
* @private: Draw the relational lines
*/
_drawRelationLine( events ) {
// let _opts = this._config,
let _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 = {},
_strokeColor = EventParams.bdColor,
_radius = this.numRound( Math.min( _props.scaleSize, _props.rowSize ) / 2, 2 )//,
// _subRadius = this.numRound( this._getPointerSize( evt.size, _opts.marginHeight ) / 2, 2 )
// Defaults
if ( _strokeColor === Default.colorScheme.event.border && Default.colorScheme.event.border !== this._config.colorScheme.event.border ) {
_strokeColor = this._config.colorScheme.event.border
}
ctx_relations.strokeStyle = _strokeColor
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 ) {
let _hep = _ey - _sy >= 0 ? _sy + _radius : _sy - _radius
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _hep ) // "┌"
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
default:
ctx_relations.moveTo( _sx, _sy )
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 ) => {
//console.log( '!_drawRelationLine:', evt )
let _rel = evt.relation,
_sx, _sy, _ex, _ey,
_targetId, _targetEvent
if ( Object.hasOwnProperty.call( _rel, 'before' ) ) {
// before: targetEvent[ _ex, _ey ] <---- selfEvent[ _sx, _sy ]
// (:> before: 自分を起点( _sx, _sy )として左方向の連結点( _ex, _ey )へ向かう描画方式
_sx = _rel.x + this.numRound( evt.margin / 2, 2 )
_sy = _rel.y + this.numRound( evt.margin / 2, 2 )
_targetId = parseInt( _rel.before, 10 )
if ( _targetId < 0 ) {
_ex = 0
_ey = _sy + this.numRound( evt.margin / 2, 2 )
} 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 + this.numRound( evt.margin / 2, 2 )
_ey = _targetEvent.relation.y + this.numRound( evt.margin / 2, 2 )
}
}
if ( _sx >= 0 && _sy >= 0 && _ex >= 0 && _ey >= 0 ) {
drawLine( _sx, _sy, _ex, _ey, evt, 'before' )
}
}
if ( Object.hasOwnProperty.call( _rel, 'after' ) ) {
// after: selfEvent[ _sx, _sy ] ----> targetEvent[ _ex, _ey ]
// (:> after: 自分を起点( _sx, _sy )として右方向の連結点( _ex, _ey )へ向かう描画方式
_sx = _rel.x + this.numRound( evt.margin / 2, 2 )
_sy = _rel.y + this.numRound( evt.margin / 2, 2 )
_targetId = parseInt( _rel.after, 10 )
if ( _targetId < 0 ) {
_ex = _props.fullwidth
_ey = _sy + this.numRound( evt.margin / 2, 2 )
} 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 + this.numRound( evt.margin / 2, 2 )
_ey = _targetEvent.relation.y + this.numRound( evt.margin / 2, 2 )
}
}
if ( _sx >= 0 && _sy >= 0 && _ex >= 0 && _ey >= 0 ) {
drawLine( _sx, _sy, _ex, _ey, evt, 'after' )
}
}
})
}
/*
* @private: Output a marker of the present time
*/
_viewPresentTime() {
let _elem = this._element,
_props = this._instanceProps,
_nowDt = new Date()
if ( this.diffDate( _props.begin, _nowDt ) < 0 || this.diffDate( _nowDt, _props.end ) < 0 ) {
return
}
let _marker = $('', { class: ClassName.PRESENT_TIME_MARKER,
style: `left:${this.numRound( this._getCoordinateX( _nowDt ), 2 )}px;top:${$(_elem).find(Selector.TIMELINE_RULER_TOP).height()}px;height:${$(_elem).find(Selector.TIMELINE_EVENT_CONTAINER).height()}px;`,
})
$(_elem).find(Selector.TIMELINE_MAIN).append( _marker )
}
/*
* @private: Retrieve the mapping data that placed current events
*/
_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
}
/*
* @private: Event when focus or blur
*/
_activeEvent( event ) {
this._debug( '_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' )
}
}
/*
* @private: Event when scroll timeline
*/
_scrollTimeline( event ) {
this._debug( '_scrollTimeline@Event' )
let _elem = event.target
this._debug( _elem.scrollLeft )
}
/*
* @private: Event when touchstart or mousedown on the timeline container
*/
_swipeStart( event ) {
this._debug( '_swipeStart@Event' )
event.preventDefault()
let _props = this._instanceProps
_props.absX = IS_TOUCH ? event.changedTouches[0].pageX : event.pageX
_props.moveX = $(event.currentTarget).parent(Selector.TIMELINE_CONTAINER).scrollLeft() * -1
this._isTouched = true
}
/*
* @private: Event when touchmove or mousemove in the timeline container
*/
_swipeMove( event ) {
if ( ! this._isTouched ) {
return
}
this._debug( '_swipeMove@Event' )
event.preventDefault()
let _props = this._instanceProps
_props.moveX -= _props.absX - ( IS_TOUCH ? event.changedTouches[0].pageX : event.pageX )
$(event.currentTarget).parent(Selector.TIMELINE_CONTAINER).scrollLeft( _props.moveX * -1 )
_props.absX = IS_TOUCH ? event.changedTouches[0].pageX : event.pageX
}
/*
* @private: Event when touchend or mouseup from the timeline container
*/
_swipeEnd() {
if ( ! this._isTouched ) {
return
}
this._debug( '_swipeEnd@Event' )
this._isTouched = false
}
/*
* @private: Event when hover on the pointer type event
*/
_hoverPointer( event ) {
if ( ! this._config.effects.hoverEvent ) {
return
}
this._debug( '_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
//this._getPointerSize( new_event.size, new_event.margin )
if ( 'mouseenter' === event.type ) {
_w = Math.min( this.numRound( _w * 1.25, '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 )
}
/*
* @private: Logger of errors when the method execution
*/
_error( message, type = 'error' ) {
if ( message && window.console ) {
type = window.console[type] ? type : 'error'
console[type]( message )
}
}
/*
* @private: Echo the log of plugin for debugging
*/
_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
/*
* @public: This method is able to call only once after completed an initializing of the plugin
*/
initialized( ...args ) {
let _message = this._isInitialized ? 'Skipped because method "initialized" already has been called once' : 'initialized'
this._debug( _message )
//console.log('!this._isInitialized:', this._isInitialized, 'this._isCompleted:', this._isCompleted )
if ( this._isInitialized ) {
return
}
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 ? this.getUserArg( _args.slice(1) ) : undefined
// 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
return this
}
/*
* @public: Destroy the object to which the plugin is applied
*/
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]
}
}
/*
* @public: 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' )
}
/*
* @public: Show hidden timeline
*/
show() {
this._debug( 'show' )
let _elem = this._element
if ( ! this._isShown ) {
$(_elem).removeClass( ClassName.HIDE )
this._isShown = true
}
}
/*
* @public: Hide shown timeline
*/
hide() {
this._debug( 'hide' )
let _elem = this._element
if ( this._isShown ) {
$(_elem).addClass( ClassName.HIDE )
this._isShown = false
}
}
/*
* @public: Move shift or expand the range of timeline container as to past direction (to left)
*/
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 ? this.getUserArg( _args.slice(2) ) : undefined,
newOpts = {},
begin_date, end_date, _tmpDate
if ( this.is_empty( moveOpts ) ) {
moveOpts = { scale: _opts.scale, range: _opts.range, shift: true }
} else {
if ( ! Object.hasOwnProperty.call( moveOpts, 'shift' ) || moveOpts.shift !== false ) {
moveOpts.shift = true
}
if ( ! Object.hasOwnProperty.call( moveOpts, 'scale' ) || ! this.verifyScale( moveOpts.scale ) ) {
moveOpts.scale = _opts.scale
}
if ( ! Object.hasOwnProperty.call( moveOpts, 'range' ) || ( ! _opts.disableLimitter ? parseInt( moveOpts.range, 10 ) > LimitScaleGrids[moveOpts.scale] : true ) ) {
moveOpts.range = _opts.range
}
}
_tmpDate = new Date( _opts.startDatetime )
switch ( true ) {
case /^years?$/i.test( moveOpts.scale ):
begin_date = new Date( _tmpDate.setFullYear( _tmpDate.getFullYear() - parseInt( moveOpts.range, 10 ) ) )
break
case /^months?$/i.test( moveOpts.scale ):
begin_date = new Date( _tmpDate.setMonth( _tmpDate.getMonth() - parseInt( moveOpts.range, 10 ) ) )
break
default:
begin_date = new Date( _tmpDate.getTime() - ( this.verifyScale( moveOpts.scale, _tmpDate.getTime(), _tmpDate.getTime(), false ) * parseInt( moveOpts.range, 10 ) ) )
break
}
newOpts.startDatetime = begin_date.toString()
if ( moveOpts.shift ) {
_tmpDate = new Date( _opts.endDatetime )
switch ( true ) {
case /^years?$/i.test( moveOpts.scale ):
end_date = new Date( _tmpDate.setFullYear( _tmpDate.getFullYear() - parseInt( moveOpts.range, 10 ) ) )
break
case /^months?$/i.test( moveOpts.scale ):
end_date = new Date( _tmpDate.setMonth( _tmpDate.getMonth() - parseInt( moveOpts.range, 10 ) ) )
break
default:
end_date = new Date( _tmpDate.getTime() - ( this.verifyScale( moveOpts.scale, _tmpDate.getTime(), _tmpDate.getTime(), false ) * parseInt( moveOpts.range, 10 ) ) )
break
}
newOpts.endDatetime = end_date.toString()
}
if ( moveOpts.scale !== _opts.scale ) {
newOpts.moveScale = moveOpts.scale
}
//console.log( '!dateback::', moveOpts, _opts.startDatetime, _opts.endDatetime, newOpts )
this.reload( [newOpts] )
if ( callback ) {
this._debug( 'Fired your callback function after datebacking.' )
callback( this._element, _opts, userdata )
}
}
/*
* @public: Move shift or expand the range of timeline container as to futrue direction (to right)
*/
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 ? this.getUserArg( _args.slice(2) ) : undefined,
newOpts = {},
begin_date, end_date, _tmpDate
if ( this.is_empty( moveOpts ) ) {
moveOpts = { scale: _opts.scale, range: _opts.range, shift: true }
} else {
if ( ! Object.hasOwnProperty.call( moveOpts, 'shift' ) || moveOpts.shift !== false ) {
moveOpts.shift = true
}
if ( ! Object.hasOwnProperty.call( moveOpts, 'scale' ) || ! this.verifyScale( moveOpts.scale ) ) {
moveOpts.scale = _opts.scale
}
if ( ! Object.hasOwnProperty.call( moveOpts, 'range' ) || ( ! _opts.disableLimitter ? parseInt( moveOpts.range, 10 ) > LimitScaleGrids[moveOpts.scale] : true ) ) {
moveOpts.range = _opts.range
}
}
_tmpDate = new Date( _opts.endDatetime )
switch ( true ) {
case /^years?$/i.test( moveOpts.scale ):
//console.log(_tmpDate, _tmpDate.getTime(), _tmpDate.getFullYear(), _tmpDate.setFullYear(_tmpDate.getFullYear() + parseInt( moveOpts.range, 10 ) ) )
end_date = new Date( _tmpDate.setFullYear( _tmpDate.getFullYear() + parseInt( moveOpts.range, 10 ) ) )
break
case /^months?$/i.test( moveOpts.scale ):
end_date = new Date( _tmpDate.setMonth( _tmpDate.getMonth() + parseInt( moveOpts.range, 10 ) ) )
break
default:
end_date = new Date( _tmpDate.getTime() + ( this.verifyScale( moveOpts.scale, _tmpDate.getTime(), _tmpDate.getTime(), false ) * parseInt( moveOpts.range, 10 ) ) )
break
}
newOpts.endDatetime = end_date.toString()
if ( moveOpts.shift ) {
_tmpDate = new Date( _opts.startDatetime )
switch ( true ) {
case /^years?$/i.test( moveOpts.scale ):
begin_date = new Date( _tmpDate.setFullYear( _tmpDate.getFullYear() + parseInt( moveOpts.range, 10 ) ) )
break
case /^months?$/i.test( moveOpts.scale ):
begin_date = new Date( _tmpDate.setMonth( _tmpDate.getMonth() + parseInt( moveOpts.range, 10 ) ) )
break
default:
begin_date = new Date( _tmpDate.getTime() + ( this.verifyScale( moveOpts.scale, _tmpDate.getTime(), _tmpDate.getTime(), false ) * parseInt( moveOpts.range, 10 ) ) )
break
}
newOpts.startDatetime = begin_date.toString()
}
if ( moveOpts.scale !== _opts.scale ) {
newOpts.moveScale = moveOpts.scale
}
//console.log( '!dateforth::', moveOpts, _opts.startDatetime, _opts.endDatetime, newOpts )
this.reload( [newOpts] )
if ( callback ) {
this._debug( 'Fired your callback function after dateforthing.' )
callback( this._element, this._config, userdata )
}
}
/*
* @public: Move the display position of the timeline container to the specified position
*/
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 )
}
}
/*
* @public: 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' )
}
/*
* @public: Add new events to the rendered timeline object
*/
async 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 ? this.getUserArg( _args.slice(2) ) : undefined,
_cacheEvents = this._loadToCache(),
_cacheIds = _cacheEvents.map((evt) => evt.eventId),
lastEventId = 0,
addedEvents = [],
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 )
lastEventId = Math.max( ..._cacheIds )
}
//console.log( '!_addEvent::before:', _cacheEvents, lastEventId, callback, userdata )
//events.forEach( ( evt ) => {
events.some( ( evt ) => {
if ( ! this.is_empty( evt.eventId ) && _cacheIds.includes( evt.eventId ) ) {
this._error( `An event with the same eventID: ${evt.eventId} already exists.`, 'warn' )
return false
}
let _one_event = this._registerEventData( '', evt )
if ( ! this.is_empty( _one_event ) ) {
//console.log( '!!_addEvent::before:', _cacheIds, lastEventId, _one_event.eventId )
if ( _one_event.eventId == 0 ) {
_one_event.eventId = Math.max( lastEventId + 1, parseInt( _one_event.eventId, 10 ) )
_cacheEvents.push( _one_event )
lastEventId = parseInt( _one_event.eventId, 10 )
} else {
_cacheEvents.push( _one_event )
lastEventId = lastEventId < _one_event.eventId ? _one_event.eventId : lastEventId
}
addedEvents[_one_event.eventId] = _one_event
_cacheIds.push( _one_event.eventId )
add_done = true
}
})
//console.log( '!!!_addEvent::after:', _cacheEvents, lastEventId, callback, userdata )
if ( ! add_done ) {
return
}
this._saveToCache( _cacheEvents )
// Prevents flicker when re-placing events; Added since v2.1.0 (#51)
$(this._element).find(Selector.TIMELINE_RELATION_LINES)[0].style.opacity = 1
$(this._element).find(Selector.TIMELINE_EVENTS)[0].style.opacity = 1
await this._placeEvent()
if ( callback ) {
this._debug( 'Fired your callback function after replacing events.' )
if ( userdata ) {
callback( this._element, this._config, userdata, addedEvents )
} else {
callback( this._element, this._config, addedEvents )
}
}
}
/*
* @public: Remove events from the currently timeline object
*/
async 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 ? this.getUserArg( _args.slice(2) ) : undefined,
_cacheEvents = this._loadToCache(),
condition = {},
removedEvents = []
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 ) => {
_cacheEvents.some( ( evt, idx ) => {
let is_remove = false
switch ( condition.type ) {
case 'eventId': {
if ( parseInt( evt.eventId, 10 ) == condition.value ) {
//console.log( `!removeEvent::${condition.type}:${condition.value}:`, _cacheEvents[_idx] )
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
//console.log( `!removeEvent::${condition.type}:${condition.value.from} ~ ${condition.value.to}:`, `${evt.eventId}: ${_fromX} <= ${evt.x} <= ${_toX} ?`, _fromX <= evt.x && evt.x <= _toX )
if ( _fromX <= evt.x && evt.x <= _toX ) {
is_remove = true
}
break
}
case 'regex': {
//console.log( `!removeEvent::${condition.type}:${condition.value}:`, JSON.stringify( evt ) )
if ( condition.value.test( JSON.stringify( evt ) ) ) {
is_remove = true
}
break
}
}
if ( is_remove ) {
removedEvents[evt.eventId] = evt
_cacheEvents.splice( idx, 1 )
}
})
})
if ( removedEvents.length == 0 ) {
this._error( 'There is no event that matches the deletion condition.', 'warn' )
return
}
this._saveToCache( _cacheEvents )
// Prevents flicker when re-placing events; Added since v2.1.0 (#51)
$(this._element).find(Selector.TIMELINE_RELATION_LINES)[0].style.opacity = 1
$(this._element).find(Selector.TIMELINE_EVENTS)[0].style.opacity = 1
await this._placeEvent()
if ( callback ) {
this._debug( 'Fired your callback function after placing additional events.' )
if ( userdata ) {
callback( this._element, this._config, userdata, removedEvents )
} else {
callback( this._element, this._config, removedEvents )
}
}
}
/*
* @public: Update events on the currently timeline object
*/
async 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 ? this.getUserArg( _args.slice(2) ) : undefined,
_cacheEvents = this._loadToCache(),
_cacheIds = _cacheEvents.map((evt) => evt.eventId),
updatedEvents = [],
update_done = false
if ( this.is_empty( events ) || ! this._isCompleted || this.is_empty( _cacheEvents ) ) {
return
}
//events.forEach( ( evt ) => {
events.some( ( evt ) => {
if ( this.is_empty( evt.eventId ) ) {
this._error( 'Could not update because the eventID to be updated is not defined.', 'warn' )
return false
}
if ( ! _cacheIds.includes( evt.eventId ) ) {
this._error( `The event node with the eventID: ${evt.eventId} to update does not exist.`, 'warn' )
return false
}
let _upc_event = this._registerEventData( '', 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 ( Object.hasOwnProperty.call( _upc_event, '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
updatedEvents[_upc_event.eventId] = _upc_event
_cacheIds.push( _upc_event.eventId )
update_done = true
}
})
if ( ! update_done ) {
return
}
this._saveToCache( _cacheEvents )
// Prevents flicker when re-placing events; Added since v2.1.0 (#51)
$(this._element).find(Selector.TIMELINE_RELATION_LINES)[0].style.opacity = 1
$(this._element).find(Selector.TIMELINE_EVENTS)[0].style.opacity = 1
await this._placeEvent()
if ( callback ) {
this._debug( 'Fired your callback function after updating events.' )
if ( userdata ) {
callback( this._element, this._config, userdata, updatedEvents )
} else {
callback( this._element, this._config, updatedEvents )
}
}
}
/*
* @public: Reload the timeline with overridable any options
*/
async 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 ? this.getUserArg( _args.slice(2) ) : undefined,
_elem = this._element,
$default_evt = $(_elem).find( Selector.DEFAULT_EVENTS ),
_old_options = this._config,
_new_options = {},
_chk_scale
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 = {}
this._countEventinCell = {}
$(_elem).empty().append( $default_evt )
this._calcVars()
this.showLoader()
if ( Object.hasOwnProperty.call( this._config, 'moveScale' ) ) {
_chk_scale = this._config.moveScale
delete this._config.moveScale
} else {
_chk_scale = this._config.scale
}
if ( ! this._verifyMaxRenderableRange( _chk_scale ) ) {
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( '', evt ) )
})
}
this._isCached = this._saveToCache( _renewEvents )
} else {
this._loadEvent()
}
await this._placeEvent()
this._isCompleted = true
await this.hideLoader()
if ( callback ) {
this._debug( 'Fired your callback function after reloading timeline.' )
callback( this._element, this._config, userdata )
}
// Binding bs.popover
if ( $.fn['popover'] ) {
$('[data-toggle="popover"]').popover()
}
}
/*
* @public: 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. (:> イベントパラメータに指定したコールバックでカスタム処理をフックできます
*/
openEvent( event ) {
this._debug( 'openEvent' )
if ( ! this.is_empty( event ) && ! Object.hasOwnProperty.call( event, 'type' ) && ! Object.hasOwnProperty.call( event, 'target' ) ) {
if ( typeof event[0] === 'function' ) {
this._beforeOpenEvent = event[0]
}
}
let _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' ) ),
_cacheEvents = this._loadToCache(),
_eventData = _cacheEvents.find( ( event ) => event.uid === uid ),
_hookedState = true
if ( this.is_empty( _self ) ) {
return
}
// Generate content for viewer
let _label = $('', { class: ClassName.VIEWER_EVENT_TITLE }),
_content = $('', { class: ClassName.VIEWER_EVENT_CONTENT }),
_meta = $('', { class: ClassName.VIEWER_EVENT_META }),
_image = $('', { class: ClassName.VIEWER_EVENT_IMAGE_WRAPPER }),
_viewers = {},
_order = [ 'label', 'image', 'content', 'meta' ]
if ( ! this.is_empty( _eventData.image ) ) {
_image.append( `` )
//_viewers.push( _image.get(0) )
_viewers.image = _image.get(0)
}
if ( ! this.is_empty( _eventData.label ) ) {
_label.html( _eventData.label )
//_viewers.push( _label.get(0) )
_viewers.label = _label.get(0)
}
if ( ! this.is_empty( _eventData.content ) ) {
_content.html( _eventData.content )
//_viewers.push( _content.get(0) )
_viewers.content = _content.get(0)
}
if ( ! this.is_empty( _eventData.rangeMeta ) ) {
_meta.html( _eventData.rangeMeta )
//_viewers.push( _meta.get(0) )
_viewers.meta = _meta.get(0)
}
if ( this._beforeOpenEvent ) {
_hookedState = this._beforeOpenEvent( _eventData, _viewers ) || true
//_hookedState = _hookedState == undefined ? true : _hookedState
}
//console.log( '!openEvent::', _self, $viewer, uid, callback, _viewers, this._beforeOpenEvent, _hookedState )
if ( ! _hookedState ) {
return
}
if ( $viewer.length > 0 ) {
$viewer.each(function() {
$(this).empty() // Initialize Viewer
_order.forEach((_prop) => {
if ( Object.prototype.hasOwnProperty.call(_viewers, _prop) ) {
$(this).append( _viewers[_prop].outerHTML )
}
})
})
}
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 )
}
}
}
/*
* @public: Be zoomed in scale of the timeline that fires when any scales on the ruler is double clicked (:> ルーラー上の任意スケールをダブルクリック時に発火するスケールズームイベント
*/
zoomScale( event ) {
this._debug( 'zoomScale' )
//console.log( '!zoomScale::', event, $(event.currentTarget) )
let _elem = event.currentTarget,
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 },
millisecond: { lower: null, minGrids: 1000 }
},
getZoomScale = ( ruler_item ) => {
let [ _scl, date_seed ] = ruler_item.split('-'),
scale = this._filterScaleKeyName( _scl ),
min_grids = scaleMap[scale].minGrids,
begin_date, end_date, base_year, week_num
//console.log( '!zoomScale::getZoomScale:', ruler_item, '->', scale, ', ', date_seed, ', minGrid:', min_grids )
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 0:00:00`
end_date = new Date( this.modifyDate( begin_date, scaleMap[scale].years, 'year' ).getTime() - 1 ).toString()
break
case /^year$/i.test( scale ):
begin_date = `${date_seed}/1/1 0:00:00`
end_date = new Date( this.modifyDate( begin_date, scaleMap[scale].years, 'year' ).getTime() - 1 ).toString()
break
case /^month$/i.test( scale ):
begin_date = this.getCorrectDatetime( date_seed ).toString()
end_date = new Date( this.modifyDate( begin_date, 1, 'month' ).getTime() - 1 ).toString()
break
case /^week$/i.test( scale ):
[ base_year, week_num ] = date_seed.split(',')
begin_date = this.getFirstDayOfWeek( week_num, base_year ).toString(),
end_date = new Date( this.modifyDate( begin_date, 7, 'day' ).getTime() - 1 ).toString()
break
case /^day$/i.test( scale ):
case /^weekday$/i.test( scale ):
date_seed = 'weekday' === scale ? date_seed.substring( 0, date_seed.indexOf(',') ) : date_seed
begin_date = this.getCorrectDatetime( date_seed ).toString()
end_date = new Date( this.modifyDate( begin_date, 1, 'day' ).getTime() - 1 ).toString()
break
case /^hour$/i.test( scale ):
case /^minute$/i.test( scale ):
case /^second$/i.test( scale ):
case /^millisecond$/i.test( scale ):
default:
begin_date = this.getCorrectDatetime( date_seed ).toString()
end_date = new Date( this.modifyDate( begin_date, 1, scale ).getTime() - 1 ).toString()
break
}
scale = Object.hasOwnProperty.call( scaleMap, scale ) ? scaleMap[scale].lower : scale
//console.log( '!zoomScale::getZoomScale:', date_seed, ', to:', scale, ', beginDate:', begin_date, ', endDate:', end_date, ', minGrids:', min_grids )
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 ( ! Object.hasOwnProperty.call( this._config, '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] )
}
/*
* @public: Show the loader
*/
showLoader() {
this._debug( 'showLoader' )
let $elem = $(this._element),
_opts = this._config,
_props = this._instanceProps,
_container = $elem.find( Selector.TIMELINE_CONTAINER ),
_max_width = _props.scaleSize * _props.grids,
_min_height = _props.rowSize * _props.rows,
_loaderContainer = $('', { class: 'jqtl-loader', style: `max-width:${_max_width}px;min-height:${_min_height}px;` }),
_loaderContent = null,
_innerContent = ''
if ( _opts.loader === false ) {
return
}
if ( _container.length == 0 ) {
// To avoid jquery memory leak
_container = _container.prevObject
}
if ( $elem.find( Selector.LOADER ).length == 0 ) {
// Generate loader container
if ( $(_opts.loader).length == 0 ) {
// Set built-in loader content
_innerContent = this.is_empty( _opts.loadingMessage ) ? '' : _opts.loadingMessage
_loaderContent = $('', { class: ClassName.LOADER_ITEM }).html( _innerContent )
} else {
// Set custom loader content
_loaderContent = $(_opts.loader).clone().prop( 'hidden', false ).css( 'display', 'block' )
}
_loaderContainer.append( _loaderContent )
_container.append( _loaderContainer )
} else {
$elem.find( Selector.LOADER ).css({ width: '100%', height: '100%' })
}
// Show loader
//$elem.find( Selector.LOADER ).show('fast', () => {
$elem.find( Selector.LOADER ).attr('data-state', 'show')
//})
}
/*
* @public: Hide the loader
*/
hideLoader() {
return new Promise(( resolve ) => {
this._debug( 'hideLoader' )
let $elem = $(this._element),
_loader = $elem.find( Selector.LOADER )
//_loader.hide('fast', () => {
_loader.attr('data-state', 'hide')
//})
setTimeout(() => {
resolve()
}, 300)
})
}
/* ----------------------------------------------------------------------------------------------------------------
* Utility Api
* ----------------------------------------------------------------------------------------------------------------
*/
/*
* Determine empty that like PHP
*
* @param mixed value (required)
*
* @return bool
*/
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 mixed item (required)
*
* @return bool
*/
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 (required)
* @param object source (required)
*
* @return object output
*/
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 (required)
*
* @return bool
*/
is_iterable( obj ) {
return obj && typeof obj[Symbol.iterator] === 'function'
}
/*
* Add an @@iterator method to non-iterable object
*
* @param object obj (required)
*
* @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
}
/*
* Supplemental method for validating arguments in local scope
*
* @param mixed default_value (required)
* @param mixed opt_arg (optional)
* @param mixed opt_callback (optional; function or string of function to call)
*
* @return mixed
*/
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 int digit (optional)
*
* @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 numeric number (required)
* @param int digit (optional)
* @param string round_type (optional; defaults to "round")
*
* @return numeric
*/
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 (required)
* @param float alpha (optional; defaults to 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
}
/*
* This method is able to get the correct datetime instead of built in "new Date" on javascript. (:> JavaScriptビルトインメソッドのnew Dateに代わって正確な日時を取得する
* That is remapping to correct year if the year is 0 - 99, and supporting years BCE. (:> 0 - 99年の場合に年をリマッピングし、紀元前の年にも対応する
*
* @param mixed datetime (required; allowed an integer as milliseconds, a string as like datetime or an object instance of Date)
* @param boolean adjustTimeZoneDiff (optional; defaults to false)
*
* @return Date Object, or null if failed
*/
getCorrectDatetime( datetime, adjustTimeZoneDiff = false ) {
let normalizeDate = ( dateString ) => {
let isMinus = /^-/.test( dateString ),
_m = isMinus ? '-' : '',
_d
if ( isMinus ) {
dateString = dateString.replace(/^-/, '')
}
// for Safari and Firefox
_d = dateString.replace(/-/g, '/')
switch ( true ) {
case /^\d{1,4}\/\d{1,2}$/.test( _d ):
return `${_m}${_d}/1`
case /^.+(\.\d{1,3})$/.test( _d ):
if ( isNaN( Date.parse( _d ) ) ) {
_d = _d.replace( RegExp.$1, '' )
}
return `${_m}${_d}`
default:
return `${_m}${_d}`
}
},
getDateObject = ( datetime ) => {
let _chk_str = normalizeDate( datetime ),
_raise = 0,
_ymd, _his, _parts, _date
switch ( true ) {
case /^-?\d{1,}\/\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] ) {
_raise = Math.floor( _parts[1] / 13 )
_parts[1] = parseInt( _parts[1], 10 ) - 1 // to month index
}
let _his_base = [ 0, 0, 0 ]
if ( _his ) {
//_parts.push( ..._his.split(':') )
_parts.push( ...Object.assign( _his_base, _his.split(':') ) )
} else {
_parts.push( ..._his_base )
}
_date = new Date( new Date( ..._parts ).setFullYear( parseInt( _parts[0], 10 ) + _raise ) )
break
}
case /^-?\d+$/.test( _chk_str ):
_date = new Date( _chk_str, 0, 1, 0, 0, 0, 0 ).setFullYear( parseInt( _chk_str, 10 ) )
break
default:
_date = new Date( _chk_str.toString() )
break
}
return _date
},
_checkDate
switch ( typeof datetime ) {
case 'number':
_checkDate = new Date( datetime )
break
case 'string':
_checkDate = getDateObject( datetime )
break
case 'object':
if ( datetime instanceof Date ) {
_checkDate = datetime
}
break
}
if ( isNaN( _checkDate ) || ! _checkDate ) {
//console.warn( `"${datetime}" Cannot parse date because invalid format.` )
this._error( `"${datetime}" Cannot parse date because invalid format.`, 'warn' )
return null
}
if ( _checkDate instanceof Date === false ) {
_checkDate = new Date( _checkDate )
}
if ( adjustTimeZoneDiff ) {
let _utcDate = new Date( _checkDate.getUTCFullYear(), _checkDate.getUTCMonth(), _checkDate.getUTCDate(), _checkDate.getUTCHours(), _checkDate.getUTCMinutes(), _checkDate.getUTCSeconds(), _checkDate.getUTCMilliseconds() ),
_tzDiff = this.diffDate( _checkDate, _utcDate )
//console.log('!getCorrectDatetime::', _checkDate.toString(), _utcDate.toString(), _tzDiff )
if ( _tzDiff != 0 ) {
_checkDate = this.modifyDate( (_tzDiff > 0 ? _utcDate : _checkDate), -1 * _tzDiff, 'millisecond' )
}
//console.log('!!getCorrectDatetime::', _checkDate.toString() )
}
return _checkDate
}
/*
* Method to get week number as extension of Date object
* Note: added support for daylight savings time but needs improvement as performance has dropped
*
* @param mixed datetime (required; to be date object filtered by getCorrectDatetime method)
*
* @return mixed (return integer as week number when given valid argument, or false if failed)
*/
getWeek( datetime ) {
if ( this.is_empty( datetime ) ) {
return false
}
let firstDayIndex = this._config.firstDayOfWeek || 0,
targetDate = this.getCorrectDatetime( datetime ),
firstDayOfYear = this.getCorrectDatetime( `${targetDate.getFullYear()}/1/1` ),
//firstWeekday = firstDayOfYear.getDay(),
targetDateStr = targetDate.toDateString(),
_weekNumber = 1,
_checkDate = firstDayOfYear
for ( let i = 0; i < 367; i++ ) {
if ( i > 0 ) {
_checkDate = this.modifyDate( firstDayOfYear, i, 'day' )
}
if ( _checkDate.getDay() == firstDayIndex ) {
_weekNumber++
}
if ( _checkDate.toDateString() === targetDateStr ) {
break
}
}
return _weekNumber
}
/*
* 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 int week_number (required)
* @param int year (optional; defaults to current year)
*
* @return mixed (return Date object as the first day of week when given valid arguments, or false if failed)
*/
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(),
//_millisecInDay = 24 * 60 * 60 * 1000,
//_week_time = (week_number - 1) * _millisecInDay * 7,
//_day_offset, _tempDt, _time, _retDt
_keyDayOfWeek = firstDayOfYear,
_offset = _weekday > firstDayIndex ? _weekday - firstDayIndex : 0,
_weekNumber = _offset <= 0 ? 0 : 1,
hitDate
//console.log( `!getFirstDayOfWeek::${year}, ${week_number}:`, _keyDayOfWeek.toDateString(), `(${_weekday}) > ${firstDayIndex} ?`, _offset, _weekNumber )
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' )
}
}
//console.log( `!!getFirstDayOfWeek::${year}, ${week_number}:`, hitDate.toDateString() )
return hitDate
/*
_weekday = _weekday == 0 ? 7 : _weekday
_day_offset = 1 - _weekday
if ( 7 - _weekday + 1 < 4 ) {
_day_offset += 7
}
_tempDt = new Date( firstDayOfYear.getTime() + _day_offset * _millisecInDay )
_time = _tempDt.getTime() + _week_time
_retDt = new Date( _tempDt.setTime( _time ) )
console.log( '!!!getFirstDayOfWeek::', week_number, _retDt.toDateString() )
return _retDt
*/
}
/*
* Get the datetime shifted from the specified datetime by any fluctuation value
*
* @param mixed datetime (required; to be date object filtered by getCorrectDatetime method)
* @param int fluctuation (required; an interval value to shift from given base datetime)
* @param string scale (required; the scale of an interval value)
*
* @return mixed (return modified new Date object when given valid argument, or false if failed)
*/
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] )
//isAdjust = true
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) ) )
}
}
//console.log( 'modifyDate:', baseDate.toString(), '-[', flct, scale, ']->', newDate.toString() )
return newDate
}
/*
* Acquire the difference between two dates with the specified scale value (:> 2つの日付の差分を指定したスケール値で取得する
*
* @param mixed date1 (required; integer as milliseconds or object instanceof Date)
* @param mixed date2 (required; integer as milliseconds or object instanceof Date)
* @param string scale (optional; defaults to 'millisecond')
* @param bool absval (optional; defaults to false)
*
* @return mixed
*/
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 (required)
* @param int begin (optional; begin of range as unit millisecs that got by `Date.getTime()`)
* @param int end (optional; end of range as unit millisecs that got by `Date.getTime()`)
* @param bool isVLS (optional; whether is variable length scale, defaults to false)
*
* @return mixed (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 (required)
*
* @return string as higher scale
*/
getHigherScale( scale ) {
return this.findScale( scale, 'higher' )
}
/*
* Retrieve one lower scale
*
* @param string scale (required)
*
* @return string as lower scale
*/
getLowerScale( scale ) {
return this.findScale( scale, 'lower' )
}
/*
* Find scale matched the specified condition
*
* @param string base_scale (required)
* @param string condition (required)
*
* @return mixed 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 (required)
* @param string scale (optional; defalts to '')
* @param string locales (optional and omittable; defaults to 'en-US')
* @param object options (optional; defaults to empty object)
*
* @return mixed locale_string (return false if failure)
*/
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 = {}, // options for built-in method only
//_ext_opts = {}, // options extended for this plugin
_has_options = false,
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 - String( num ).length, '0' )
return String( num ).length == digit ? String( num ) : ( zero + num ).substr( num * -1 )
},
_prop, _temp, _str, _num, _year, _month, _week
if ( this.is_empty( date_seed ) ) {
return false
}
locales = this.supplement( 'en-US', locales, this.validateString )
options = this.supplement( {}, options, this.validateObject )
for ( _prop in options ) {
if ( /^(localeMatcher|timeZone|hour12|formatMatcher|era|timeZoneName)$/.test( _prop ) ) {
_options[_prop] = options[_prop]
}
}
if ( Object.keys( _options ).length > 0 ) {
_has_options = true
}
//console.log( `!getLocaleString::${scale}:`, date_seed, locales, options[scale], is_toLocalString )
switch ( true ) {
case /^millenniums?|millennia$/i.test( scale ):
case /^century$/i.test( scale ):
case /^dec(ade|ennium)$/i.test( scale ):
case /^lustrum$/i.test( scale ):
// Allowed value as format: 'numeric', 'ordinal'
_year = this.getCorrectDatetime( date_seed ).getFullYear()
if ( /^millenniums?|millennia$/i.test( scale ) ) {
_temp = 1000
} else
if ( /^century$/i.test( scale ) ) {
_temp = 100
} else
if ( /^dec(ade|ennium)$/i.test( scale ) ) {
_temp = 10
} else {
_temp = 5
}
_num = this.numRound( _year / _temp, 0, 'ceil' )
if ( Object.hasOwnProperty.call( options, scale ) && options[scale] === 'ordinal' ) {
locale_string = getOrdinal( _num )
} else {
locale_string = _num
}
break
case /^years?$/i.test( scale ):
// Allowed value as format: 'numeric', '2-digit', 'zerofill'
_temp = this.getCorrectDatetime( date_seed )
_year = _temp.getFullYear()
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(numeric|2-digit)$/i.test( options[scale] ) ) {
_options.year = options[scale]
//locale_string = _temp.toLocaleString( locales, _options )
locale_string = _temp.toLocaleDateString( locales, _options )
} else
if ( /^zerofill$/i.test( options[scale] ) ) {
locale_string = _year.toString().length > 3 ? _year : getZerofill( _year, 4 )
if ( _has_options ) {
locale_string = _temp.toLocaleDateString( locales, _options ).replace( _year, locale_string )
}
} else {
locale_string = _year
}
} else
if ( _has_options ) {
locale_string = _temp.toLocaleDateString( locales, _options )
}
}
locale_string = this.is_empty( locale_string ) ? _year : locale_string
//console.log(`!getLocaleString::${scale}:`, date_seed, _temp, _year, is_toLocalString, options[scale], locale_string )
break
case /^months?$/i.test( scale ):
// Allowed value as format: 'numeric', '2-digit', 'narrow', 'short', 'long'
_temp = this.getCorrectDatetime( date_seed, true )
_month = _temp.getMonth() + 1
//console.log(`!getLocaleString::${scale}:`, date_seed, _temp, _month, is_toLocalString, options[scale], _options )
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(numeric|2-digit|narrow|short|long)$/i.test( options[scale] ) ) {
_options.month = options[scale]
locale_string = _temp.toLocaleString( locales, _options )
} else {
locale_string = _month
}
} else
if ( _has_options ) {
locale_string = _temp.toLocaleDateString( locales, _options )
}
}
locale_string = this.is_empty( locale_string ) ? _month : locale_string
//console.log(`!!getLocaleString::${scale}:`, locale_string )
break
case /^weeks?$/i.test( scale ):
// Allowed value as format: 'numeric', 'ordinal'
if ( typeof date_seed === 'string' && /^(.*)+,\d{1,2}$/.test( date_seed ) ) {
[ _str, _num ] = date_seed.split(',')
_week = parseInt( _num, 10 )
} else {
_week = this.getWeek( this.getCorrectDatetime( date_seed ) )
}
if ( Object.hasOwnProperty.call( options, scale ) && options[scale] === 'ordinal' ) {
locale_string = getOrdinal( _week )
} else {
locale_string = _week
}
break
case /^weekdays?$/i.test( scale ):
// Allowed value as format: 'narrow', 'short', 'long'
if ( typeof date_seed === 'string' && /^(.*)+,\d{1}$/.test( date_seed ) ) {
[ _str, _num ] = date_seed.split(',')
_temp = this.getCorrectDatetime( _str, true )
_num = parseInt( _num, 10 )
} else {
_temp = this.getCorrectDatetime( date_seed, true )
}
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(narrow|short|long)$/i.test( options[scale] ) ) {
_options.weekday = options[scale]
locale_string = _temp.toLocaleString( locales, _options )
}
} else
if ( _has_options ) {
locale_string = _temp.toLocaleDateString( locales, _options )
}
}
if ( this.is_empty( locale_string ) ) {
_str = _temp.toLocaleDateString( locales, { weekday: 'long' } )
if ( /^short$/i.test( options[scale] ) ) {
locale_string = _str.substring(0, 3)
} else
if ( /^long$/i.test( options[scale] ) ) {
locale_string = _str
} else {
locale_string = _str.substring(0, 1)
}
}
break
case /^days?$/i.test( scale ):
// Allowed value as format: 'numeric', '2-digit', 'ordinal'
_temp = this.getCorrectDatetime( date_seed, true )
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(numeric|2-digit)$/i.test( options[scale] ) ) {
_options.day = options[scale]
locale_string = _temp.toLocaleString( locales, _options )
} else
if ( /^ordinal$/i.test( options[scale] ) ) {
locale_string = getOrdinal( parseInt( _temp.getDate(), 10 ) )
}
} else
if ( _has_options ) {
locale_string = _temp.toLocaleDateString( locales, _options )
}
}
locale_string = this.is_empty( locale_string ) ? _temp.getDate() : locale_string
break
case /^hours?$/i.test( scale ):
case /^(half|quarter)-?hours?$/i.test( scale ):
// Allowed value as format: 'numeric', '2-digit', 'fulltime'
_temp = this.getCorrectDatetime( date_seed )
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(numeric|2-digit)$/i.test( options[scale] ) ) {
_options.hour = options[scale]
} else
if ( /^fulltime$/i.test( options[scale] ) ) {
_options.hour = 'numeric'
_options.minute = 'numeric'
}
locale_string = _temp.toLocaleString( locales, _options )
} else
if ( _has_options ) {
locale_string = _temp.toLocaleString( locales, _options )
}
}
locale_string = this.is_empty( locale_string ) ? _temp.getHours() : locale_string
break
case /^minutes?$/i.test( scale ):
// Allowed value as format: 'numeric', '2-digit', 'fulltime'
_temp = this.getCorrectDatetime( date_seed )
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(numeric|2-digit)$/i.test( options[scale] ) ) {
_options.minute = options[scale]
} else
if ( /^fulltime$/i.test( options[scale] ) ) {
_options.hour = 'numeric'
_options.minute = 'numeric'
}
locale_string = _temp.toLocaleString( locales, _options )
} else
if ( _has_options ) {
locale_string = _temp.toLocaleString( locales, _options )
}
}
locale_string = this.is_empty( locale_string ) ? _temp.getMinutes() : locale_string
break
case /^seconds?$/i.test( scale ):
// Allowed value as format: 'numeric', '2-digit', 'fulltime'
_temp = this.getCorrectDatetime( date_seed )
if ( is_toLocalString ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( /^(numeric|2-digit)$/i.test( options[scale] ) ) {
_options.second = options[scale]
} else
if ( /^fulltime$/i.test( options[scale] ) ) {
_options.hour = 'numeric'
_options.minute = 'numeric'
_options.second = 'numeric'
}
locale_string = _temp.toLocaleString( locales, _options )
} else
if ( _has_options ) {
locale_string = _temp.toLocaleString( locales, _options )
}
}
locale_string = this.is_empty( locale_string ) ? _temp.getSeconds() : locale_string
break
case /^millisec(|ond)s?$/i.test( scale ):
// Allowed value as format: 'narrow', 'numeric'
_temp = this.getCorrectDatetime( date_seed )
if ( Object.hasOwnProperty.call( options, scale ) ) {
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( /^numeric$/i.test( options[scale] ) ) {
locale_string = parseInt( _temp.getMilliseconds(), 10 )
} else {
locale_string = getZerofill( parseInt( _temp.getMilliseconds(), 10 ), 3 )
}
}
locale_string = this.is_empty( locale_string ) ? _temp.getMilliseconds() : locale_string
break
case /^custom$/i.test( scale ):
//console.log( `!getLocaleString::${scale}:`, date_seed, locales, options, _options, _has_options )
// Custom format
_temp = this.getCorrectDatetime( date_seed )
if ( Object.hasOwnProperty.call( options, 'timeZone' ) && /^utc$/i.test(options.timeZone) ) {
_temp = this.modifyDate( _temp, -1 * _temp.getTimezoneOffset(), 'minute' )
}
if ( Object.hasOwnProperty.call( options, scale ) ) {
locale_string = this.datetimeFormat( _temp, options[scale], locales )
}
locale_string = this.is_empty( locale_string ) ? _temp.toString() : locale_string
break
default:
// Allowed value as format: 'narrow'
_temp = this.getCorrectDatetime( date_seed )
if ( _has_options ) {
locale_string = _temp.toLocaleString( locales, _options )
} else {
locale_string = _temp.toString()
}
break
}
//console.log( '!getLocaleString:', date_seed, scale, locales, options[scale], locale_string )
return locale_string.toString()
}
/*
* Convert the date-time to custom formatting strings, as like ruby
*
* @param mixed baseDate (required; should be a Date object)
* @param string format (optional; defaults to '')
* @param string locales (optional; defaults to '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 (required)
*
* @return int
*/
strWidth( str ) {
let _str_ruler = $( '' ),
_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)
* Usage: Object.sort( this.compareValues( property, order ) )
*
* @param string key (required)
* @param string order (optional; defaults to 'asc')
*
* @return object
*/
compareValues( key, order = 'asc' ) {
return ( a, b ) => {
if ( ! Object.hasOwnProperty.call( a, key ) || ! Object.hasOwnProperty.call( b, key ) ) {
return 0
}
const varA = typeof a[key] === 'string' ? a[key].toUpperCase() : a[key]
const varB = typeof b[key] === 'string' ? b[key].toUpperCase() : b[key]
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 array userdata (required)
*
* @return mixed
*/
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 = $('', { 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 ) )
}
/*
* Validators
*/
validateString( def, val ) {
return typeof val === 'string' && val !== '' ? val : def
}
validateNumeric( def, val ) {
return typeof val === 'number' ? Number( val ) : def
}
validateBoolean( def, val ) {
return typeof val === 'boolean' || ( typeof val === 'object' && val !== null && typeof val.valueOf() === 'boolean' ) ? val : def
}
validateObject( def, val ) {
return typeof val === 'object' ? val : def
}
validateArray( def, val ) {
return Object.prototype.toString.call( val ) === '[object Array]' ? val : def
}
// Static
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._isCompleted ) {
data._init()
}
}
})
}
static _getInstance( element ) {
return Data.getData( element, DATA_KEY )
}
} // class end
/* ----------------------------------------------------------------------------------------------------------------
* For jQuery
* ----------------------------------------------------------------------------------------------------------------
*/
$.fn[NAME] = Timeline._jQueryInterface
$.fn[NAME].Constructor = Timeline
$.fn[NAME].noConflict = () => {
$.fn[NAME] = JQUERY_NO_CONFLICT
return Timeline._jQueryInterface
}