Widget:Event-Timer/script.js: Unterschied zwischen den Versionen
Zur Navigation springen
Zur Suche springen
Aylia (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Think (Diskussion | Beiträge) (Skript-Update mit der neusten Version aus EN) |
||
Zeile 2: | Zeile 2: | ||
/* Guild Wars 2 Wiki: Event timer */ | /* Guild Wars 2 Wiki: Event timer */ | ||
// Increment this every time a release is added to invalidate the existing sequence and force users to load the new map timer. | // Increment this every time a release is added to invalidate the existing sequence and force users to load the new map timer. | ||
var version = 'v3. | var version = 'v3.7.0'; // November 2023: Added logic path to reset sequence if all non-toptime event bars have been removed using the [x]. | ||
// GLOBAL VARIABLES | // GLOBAL VARIABLES | ||
Zeile 625: | Zeile 625: | ||
var customEventData = {}; | var customEventData = {}; | ||
// Sequence in which the elements will render | // Sequence in which the elements will render. | ||
var defaultSequence = Object.keys(eventData); | var defaultSequence = Object.keys(eventData); | ||
// If there are more than 10 elements showing, it's probably a long way between the first times and the last, so add another to the end. | // If there are more than 10 elements showing, it's probably a long way between the first times and the last, so add another to the end. | ||
Zeile 641: | Zeile 634: | ||
// Calculate the user timezone offset for continued later use. Globally track start hour too. | // Calculate the user timezone offset for continued later use. Globally track start hour too. | ||
var now = new Date(), timezoneOffset = (-1 * now.getTimezoneOffset() ), startHourUTC, twelveHourTimes, setIntervalHandle, otherHourOffset = 0, usedHeadings = []; | var now = new Date(), timezoneOffset = (-1 * now.getTimezoneOffset()), startHourUTC, twelveHourTimes, setIntervalHandle, otherHourOffset = 0, usedHeadings = []; | ||
// UTILITY FUNCTIONS | // UTILITY FUNCTIONS | ||
Zeile 647: | Zeile 640: | ||
function writeTimerCSS() { | function writeTimerCSS() { | ||
// ** Sheet 2 - Event colour scheme ** | // ** Sheet 2 - Event colour scheme ** | ||
var cssText = $.map(eventData, function(metaEventData, metaKey) { | var cssText = $.map(eventData, function (metaEventData, metaKey) { | ||
var x; | |||
return $.map(metaEventData.segments, function(v, k) { | return $.map(metaEventData.segments, function (v, k) { | ||
x = ''; | x = ''; | ||
switch (typeof v.bg) { | switch (typeof v.bg) { | ||
case 'object': | |||
switch (v.bg.length) { | |||
case 2: // linear-gradients | |||
x = '.event-bar-segment.' + metaKey + k + ' { background: linear-gradient(90deg, rgb(' + v.bg[0].join(',') + '), rgb(' + v.bg[1].join(',') + ')) }'; | |||
break; | |||
case 3: | |||
x = ['.event-bar-segment.' + metaKey + k + ' { background: rgb(' + v.bg.join(',') + ') }', | |||
'.event-bar-segment.' + metaKey + k + '.future { background: rgba(' + v.bg.join(',') + ',0.3) }']; | |||
break; | |||
} | |||
break; | |||
case 'string': // transparent or other alternative text | |||
x = '.event-bar-segment.' + metaKey + k + ' { background: ' + v.bg + '}'; | |||
break; | |||
} | } | ||
return x; | return x; | ||
}); | }); | ||
}).join('\n'); | }).join('\n'); | ||
$('#EventTimerCSS2').text('/* Widget:Event | $('#EventTimerCSS2').text('/* Widget:Event timer - Stylesheet 1 */\n' + cssText); | ||
// ** Sheet 3 - Compact window width ** | // ** Sheet 3 - Compact window width ** | ||
Zeile 678: | Zeile 671: | ||
// And rerun when the window changes size | // And rerun when the window changes size | ||
var resizeTimer; | var resizeTimer; | ||
$(window).resize(function() { | $(window).resize(function () { | ||
clearTimeout(resizeTimer); | |||
resizeTimer = setTimeout(fitTimerToWindowWidth, 250); | |||
}); | }); | ||
} | } | ||
Zeile 686: | Zeile 679: | ||
// Utility function #2 and #3: HTML5 localStorage operator functions used to request existing preferences, and store user preferences for later visits | // Utility function #2 and #3: HTML5 localStorage operator functions used to request existing preferences, and store user preferences for later visits | ||
function getEventTimerPreferences(keyname, defaultvalue) { | function getEventTimerPreferences(keyname, defaultvalue) { | ||
var response = JSON.parse(localStorage.getItem('event-timer-'+keyname)); | var response = JSON.parse(localStorage.getItem('event-timer-' + keyname)); | ||
if (typeof response == 'undefined' || response == null ) { response = defaultvalue; } | if (typeof response == 'undefined' || response == null) { response = defaultvalue; } | ||
return response; | return response; | ||
} | } | ||
Zeile 702: | Zeile 695: | ||
} | } | ||
try { | try { | ||
localStorage.removeItem('event-timer-'+keyname); | localStorage.removeItem('event-timer-' + keyname); | ||
localStorage.setItem('event-timer-'+keyname, JSON.stringify(value)); | localStorage.setItem('event-timer-' + keyname, JSON.stringify(value)); | ||
console.log('Changed preference: ', keyname); | console.log('Changed preference: ', keyname); | ||
} | } | ||
Zeile 715: | Zeile 708: | ||
function addCheckbox(keyname, desc, hoverdesc, defaultvalue) { | function addCheckbox(keyname, desc, hoverdesc, defaultvalue) { | ||
hoverdesc += ' >> ' + uitext.checkboxhover; | hoverdesc += ' >> ' + uitext.checkboxhover; | ||
var box = $( | var box = $(document.createElement("input")).attr("type", "checkbox").attr("id", keyname + "-toggle").attr("title", hoverdesc); | ||
var label = $( | var label = $(document.createElement("label")).attr("for", keyname + "-toggle").attr("title", hoverdesc).text(desc); | ||
box.attr('checked', getEventTimerPreferences(keyname, defaultvalue) ); | box.attr('checked', getEventTimerPreferences(keyname, defaultvalue)); | ||
eventTimerSettings.append(box).append(label); | eventTimerSettings.append(box).append(label); | ||
} | } | ||
// Create fieldset container with legend | // Create fieldset container with legend | ||
var eventTimerSettings = $( | var eventTimerSettings = $(document.createElement("fieldset")).attr("class", "widget").attr("id", "event-timer-legend") | ||
$.each(uitext.checkboxes, function(k,v) { | .append($(document.createElement("legend")).text(uitext.legendname)); | ||
$.each(uitext.checkboxes, function (k, v) { | |||
addCheckbox(k, v.name, v.hover, v.defaultvalue); | addCheckbox(k, v.name, v.hover, v.defaultvalue); | ||
}); | }); | ||
eventTimerSettings.append($( | eventTimerSettings.append($(document.createElement("input")).attr("id", "apply-button").attr("class", "mw-ui-button button").attr("type", "button").attr("value", uitext.applysettings)); | ||
eventTimerSettings.append($( | eventTimerSettings.append($(document.createElement("input")).attr("id", "forget-button").attr("class", "mw-ui-button button").attr("type", "button").attr("value", uitext.forgetsettings)); | ||
eventTimerSettings.append($( | eventTimerSettings.append($(document.createElement("span")) | ||
.append(wikiLink(uitext.widgetlink, uitext.widgetlinktext)) | |||
); | |||
$('#event-wrapper').after(eventTimerSettings); | $('#event-wrapper').after(eventTimerSettings); | ||
// Save checkbox settings | // Save checkbox settings | ||
$.each(uitext.checkboxes, function(k,v) { | $.each(uitext.checkboxes, function (k, v) { | ||
$('#' + k + '-toggle').click(function() { | $('#' + k + '-toggle').click(function () { | ||
setEventTimerPreferences(k, $('#' + k + '-toggle').prop('checked'), v.defaultvalue); | setEventTimerPreferences(k, $('#' + k + '-toggle').prop('checked'), v.defaultvalue); | ||
}); | }); | ||
}); | }); | ||
$('#apply-button').click(function() { | $('#apply-button').click(function () { | ||
mainEventTimer(true); | mainEventTimer(true); | ||
}); | }); | ||
$('#forget-button').click(function() { | $('#forget-button').click(function () { | ||
try { | try { | ||
// Remove local storage and reset checkboxes | // Remove local storage and reset checkboxes | ||
localStorage.removeItem('event-timer-version'); | localStorage.removeItem('event-timer-version'); | ||
localStorage.removeItem('event-timer-sequence'); | localStorage.removeItem('event-timer-sequence'); | ||
$.each(uitext.checkboxes, function(k,v) { | $.each(uitext.checkboxes, function (k, v) { | ||
localStorage.removeItem('event-timer-' + k); | localStorage.removeItem('event-timer-' + k); | ||
$('#' + k + '-toggle').prop('checked', v.defaultvalue); | $('#' + k + '-toggle').prop('checked', v.defaultvalue); | ||
Zeile 764: | Zeile 760: | ||
// Calculate the hours and minutes | // Calculate the hours and minutes | ||
var hour = Math.floor( time/60 ); | var hour = Math.floor(time / 60); | ||
var minute = time % 60; | var minute = time % 60; | ||
Zeile 781: | Zeile 777: | ||
if (twelveHourTimes == false) { | if (twelveHourTimes == false) { | ||
timeRaw = pad(date.getHours()) + ':' + pad(date.getMinutes()); | timeRaw = pad(date.getHours()) + ':' + pad(date.getMinutes()); | ||
timeString = | timeString = $(document.createElement("span")).attr("title", uitext.timezonehover + " (UTC" + (timezoneOffset < 0 ? timezoneOffset / 60 : "+" + timezoneOffset / 60) + ")").text(pad(date.getHours()) + ':' + pad(date.getMinutes())); | ||
} else { | } else { | ||
timeRaw = (((date.getHours() + 11) % 12) + 1) + ':' + pad(date.getMinutes()) + ' ' + (date.getHours() >= 12 ? 'PM' : 'AM'); | timeRaw = (((date.getHours() + 11) % 12) + 1) + ':' + pad(date.getMinutes()) + ' ' + (date.getHours() >= 12 ? 'PM' : 'AM'); | ||
timeString = | timeString = $(document.createElement("span")).attr("title", uitext.timezonehover + " (UTC" + (timezoneOffset < 0 ? timezoneOffset / 60 : "+" + timezoneOffset / 60) + ")").text((((date.getHours() + 11) % 12) + 1) + ":" + pad(date.getMinutes()) + " " + (date.getHours() >= 12 ? "PM" : "AM")); | ||
} | } | ||
} | } | ||
Zeile 826: | Zeile 822: | ||
// Utility function #8: Draw blocks for the given object | // Utility function #8: Draw blocks for the given object | ||
function drawRow(metaKey, metaSingular ) { | function drawRow(metaKey, metaSingular) { | ||
// Create a bar container (this will hold the bar and the associated title) | // Create a bar container (this will hold the bar and the associated title) | ||
var barcontainer = $( | var barcontainer = $(document.createElement("div")).attr("class", "event-bar-container " + metaKey).attr("data-abbr", metaKey); | ||
// Display category if not used before | // Display category if not used before | ||
if ( typeof metaSingular.category != 'undefined' && usedHeadings.indexOf( metaSingular.category ) == -1 ) { | if (typeof metaSingular.category != 'undefined' && usedHeadings.indexOf(metaSingular.category) == -1) { | ||
usedHeadings.push( metaSingular.category ); | usedHeadings.push(metaSingular.category); | ||
barcontainer.append($( | barcontainer.append($(document.createElement("h3")).attr("class", metaKey).text(metaSingular.category)); | ||
} | } | ||
// Display heading with link always | // Display heading with link always | ||
if ( typeof metaSingular.link != 'undefined' ) { | if (typeof metaSingular.link != 'undefined') { | ||
barcontainer.append($( | barcontainer.append($(document.createElement("h4")) | ||
.append($(document.createElement("span")) | |||
.append(wikiLink(metaSingular.link, metaSingular.name)) | |||
) | |||
); | |||
} else { | } else { | ||
barcontainer.append($( | barcontainer.append($(document.createElement("h4")) | ||
.append($(document.createElement("span")) | |||
.append(wikiLink(metaSingular.name)) | |||
) | |||
); | |||
} | } | ||
// Create a bar for the meta segments | // Create a bar for the meta segments | ||
var bar = $( | var bar = $(document.createElement("div")).attr("class", "event-bar"); | ||
// For each event create a "segment" | // For each event create a "segment" | ||
$.each( metaSingular.sequences.refined, function(i, v) { | $.each(metaSingular.sequences.refined, function (i, v) { | ||
var name = metaSingular.segments[v.r].name, link = metaSingular.segments[v.r].link || ( name == '' ? '' : name ), chatlink = metaSingular.segments[v.r].chatlink || ''; | var name = metaSingular.segments[v.r].name, link = metaSingular.segments[v.r].link || (name == '' ? '' : name), chatlink = metaSingular.segments[v.r].chatlink || ''; | ||
var time = unwrapUTC(v['s']); | var time = unwrapUTC(v['s']); | ||
// Create a segment to represent that phase, and set the width based on the duration | // Create a segment to represent that phase, and set the width based on the duration | ||
var segment = $( | var segment = $(document.createElement("div")).attr("class", "event-bar-segment " + metaKey + v.r + v.cl).css("width", (100 * v["d"] / 135) + "%").attr("title", (metaSingular.name ? metaSingular.name + "\r" : "") + time.raw + (name == "" ? "" : " - ") + name); | ||
// Time | // Time | ||
var segmentTime = $( | var segmentTime = $(document.createElement("span")).attr("class", "event-time") | ||
.append(time.string); | |||
segment.append(segmentTime); | segment.append(segmentTime); | ||
// Segment event name and link | // Segment event name and link | ||
segment.append( | // Use NBSP instead of leaving empty event names, as if the name is blank for a whole 2hr15 slot, then the segment height will reduce if the span element is empty | ||
if (name == "") { name = "\u00a0"; } | |||
segment.append($(document.createElement("span")).attr("class", "event-name") | |||
.append(link == "" ? document.createTextNode(name) : wikiLink(link, name))); | |||
// Chatlink | // Chatlink | ||
if (chatlink != '') { | if (chatlink != '') { | ||
segment.append( chatLinkSelect(chatlink) ); | segment.append(chatLinkSelect(chatlink)); | ||
} | } | ||
bar.append(segment); | bar.append(segment); | ||
}); | }); | ||
bar.append( | bar.append($(document.createElement("span")).attr("class", "event-bar-exit").attr("title", uitext.deleterowhover).text("[X]")); | ||
barcontainer.append(bar); | barcontainer.append(bar); | ||
Zeile 884: | Zeile 892: | ||
// Filter the data down from 24 hours to roughly 2 hours. | // Filter the data down from 24 hours to roughly 2 hours. | ||
function timeWithinWindow(schedule) { | function timeWithinWindow(schedule) { | ||
return ( (schedule.e > ws && schedule.s < we) ); | return ((schedule.e > ws && schedule.s < we)); | ||
} | } | ||
var roughSchedule = schedule.filter(timeWithinWindow); | var roughSchedule = schedule.filter(timeWithinWindow); | ||
Zeile 890: | Zeile 898: | ||
// Refine the data to restrict lengths to visible window | // Refine the data to restrict lengths to visible window | ||
var refinedSchedule = []; | var refinedSchedule = []; | ||
$.each(roughSchedule, function(i,v) { | $.each(roughSchedule, function (i, v) { | ||
// Local copies that we can adjust | // Local copies that we can adjust | ||
var r = v.r, s = v.s, e = v.e; | var r = v.r, s = v.s, e = v.e; | ||
// Check if window starts after the segment started, if so, crop it | // Check if window starts after the segment started, if so, crop it | ||
if ( ws > s ) { | if (ws > s) { | ||
s = ws; | s = ws; | ||
if (metaKey == 'ds' && r == 1) { | if (metaKey == 'ds' && r == 1) { | ||
Zeile 903: | Zeile 911: | ||
// Check end of segment is before window end, if not, crop it | // Check end of segment is before window end, if not, crop it | ||
if ( e > we ) { | if (e > we) { | ||
e = we; | e = we; | ||
} | } | ||
// Check if segment crosses the 2 hour marker, if it does, split into two | // Check if segment crosses the 2 hour marker, if it does, split into two | ||
if ( s < wf && wf < e ) { | if (s < wf && wf < e) { | ||
// Two objects, one beginning to the left of the future line + ending at the future line, and one starting at the future line | // Two objects, one beginning to the left of the future line + ending at the future line, and one starting at the future line | ||
refinedSchedule.push({ | refinedSchedule.push({ | ||
r: r, | r: r, // Reference id, e.g. wb1 | ||
s: s, | s: s, // Start minutes, e.g. 10 | ||
e: wf, | e: wf, // End minutes, e.g. 60 | ||
d: wf - s, // Duration, e.g. 50 | d: wf - s, // Duration, e.g. 50 | ||
cl: '' | cl: '' // Class placeholder only used for future last 15 minutes segments | ||
}); | }); | ||
if (metaKey == 'ds' && r == 1) { | if (metaKey == 'ds' && r == 1) { | ||
Zeile 927: | Zeile 935: | ||
cl: ' future' | cl: ' future' | ||
}); | }); | ||
} else if ( wf < e ) { | } else if (wf < e) { | ||
// Just one object, with the ending after the future line + beginning on or after future line | // Just one object, with the ending after the future line + beginning on or after future line | ||
refinedSchedule.push({ | refinedSchedule.push({ | ||
Zeile 951: | Zeile 959: | ||
// Refine schedule to fit 135 minute view. | // Refine schedule to fit 135 minute view. | ||
$.each(metas, function(k, v) { | $.each(metas, function (k, v) { | ||
metas[k].sequences.refined = refineRow(v.sequences.full, k); | metas[k].sequences.refined = refineRow(v.sequences.full, k); | ||
}); | }); | ||
Zeile 980: | Zeile 988: | ||
// Do the work | // Do the work | ||
$.each(metaSequence, function(i,metaKey) { | $.each(metaSequence, function (i, metaKey) { | ||
drawRow(metaKey, customEventData[metaKey] ); | drawRow(metaKey, customEventData[metaKey]); | ||
}); | }); | ||
Zeile 987: | Zeile 995: | ||
$('#event-container').sortable({ | $('#event-container').sortable({ | ||
placeholder: 'ui-sortable-placeholder', | placeholder: 'ui-sortable-placeholder', | ||
update: function() { | update: function () { | ||
// Update stored values | // Update stored values | ||
var eventBars = $('.event-bar-container'); | var eventBars = $('.event-bar-container'); | ||
var eventAbbrs = []; | var eventAbbrs = []; | ||
$.each(eventBars, function() { | $.each(eventBars, function () { | ||
eventAbbrs.push(this.getAttribute('data-abbr')); | eventAbbrs.push(this.getAttribute('data-abbr')); | ||
}); | }); | ||
setEventTimerPreferences('sequence',eventAbbrs,defaultSequence); | setEventTimerPreferences('sequence', eventAbbrs, defaultSequence); | ||
console.log('Rearranged sequence to: ' + JSON.stringify(eventAbbrs)); | console.log('Rearranged sequence to: ' + JSON.stringify(eventAbbrs)); | ||
// Now reload otherwise people whine about category titles. | // Now reload otherwise people whine about category titles. | ||
mainEventTimer(true); | mainEventTimer(true); | ||
Zeile 1.003: | Zeile 1.011: | ||
// Allow closure of event bars | // Allow closure of event bars | ||
$('.event-bar-exit').click(function() { | $('.event-bar-exit').click(function () { | ||
var eventBar = this.closest('.event-bar-container'); | var eventBar = this.closest('.event-bar-container'); | ||
var eventAbbr = eventBar.getAttribute('data-abbr'); | var eventAbbr = eventBar.getAttribute('data-abbr'); | ||
var currentPref = getEventTimerPreferences('sequence',defaultSequence); | var currentPref = getEventTimerPreferences('sequence', defaultSequence); | ||
// Adjust stored preferences to remove given element from preferences | // Adjust stored preferences to remove given element from preferences | ||
var abbrIndex = currentPref.indexOf(eventAbbr); | var abbrIndex = currentPref.indexOf(eventAbbr); | ||
if ( abbrIndex > -1 ) { | if (abbrIndex > -1) { | ||
currentPref.splice(abbrIndex,1); | currentPref.splice(abbrIndex, 1); | ||
} | } | ||
setEventTimerPreferences('sequence',currentPref,defaultSequence); | setEventTimerPreferences('sequence', currentPref, defaultSequence); | ||
console.log('Deleted element. Remaining sequence: ' + JSON.stringify(currentPref)); | console.log('Deleted element. Remaining sequence: ' + JSON.stringify(currentPref)); | ||
// And hide chosen element whilst still on this page. Next time it won't load that element until you press reset. | // And hide chosen element whilst still on this page. Next time it won't load that element until you press reset. | ||
// $('[data-abbr="'+eventAbbr+'"]').remove(); -- not required if we redraw | // $('[data-abbr="'+eventAbbr+'"]').remove(); -- not required if we redraw | ||
// Check if remaining sequence is only length 2 (i.e. only the the top and bottom times remain - everything else deleted) | |||
// If so, reset the sequence entirely. | |||
var revisedCurrentPref = getEventTimerPreferences('sequence', defaultSequence); | |||
if (revisedCurrentPref.length === 2) { | |||
setEventTimerPreferences('sequence', defaultSequence); | |||
} | |||
// Now reload otherwise people whine about category titles. | // Now reload otherwise people whine about category titles. | ||
Zeile 1.029: | Zeile 1.044: | ||
// Utility function #11: Generate a full day of meta pattern | // Utility function #11: Generate a full day of meta pattern | ||
function eventsGenerator(eventData, metaSequence) { | function eventsGenerator(eventData, metaSequence) { | ||
function fullPatternGenerator(partial, pattern){ | function fullPatternGenerator(partial, pattern) { | ||
// 23:00 plus 2 hour lookahead plus 15 mins future | |||
// | var fillDuration = 60 * 25 + 15; | ||
var fillDuration = 60*25 + 15; | |||
// Figure out total length of partial | // Figure out total length of partial | ||
var partialDuration = 0; $.map(partial, function(v){ partialDuration += v.d; }); | var partialDuration = 0; $.map(partial, function (v) { partialDuration += v.d; }); | ||
// If already sufficiently long, then we don't need to add any pattern sections | // If already sufficiently long, then we don't need to add any pattern sections | ||
var fullPattern; | var fullPattern; | ||
if ( partialDuration >= fillDuration ) { | if (partialDuration >= fillDuration) { | ||
fullPattern = partial; | fullPattern = partial; | ||
} else { | } else { | ||
// Figure out total length of pattern | // Figure out total length of pattern | ||
var patternDuration = 0; $.map(pattern, function(v){ patternDuration += v.d; }); | var patternDuration = 0; $.map(pattern, function (v) { patternDuration += v.d; }); | ||
// Minimum number of pattern repetitions required | // Minimum number of pattern repetitions required | ||
var patternQty = Math.ceil( (fillDuration - partialDuration) / patternDuration); | var patternQty = Math.ceil((fillDuration - partialDuration) / patternDuration); | ||
// Repeat pattern - can use this when we remove IE support later: | // Repeat pattern - can use this when we remove IE support later: | ||
// var repeatedPattern = Array(patternQty).fill().map(function(){ return pattern; }); | // var repeatedPattern = Array(patternQty).fill().map(function(){ return pattern; }); | ||
var repeatedPattern = 'z'.repeat(patternQty).split('').map(function() { return pattern; }); | var repeatedPattern = 'z'.repeat(patternQty).split('').map(function () { return pattern; }); | ||
// Collapse nested arrays and concatenate with the initial partial pattern | // Collapse nested arrays and concatenate with the initial partial pattern | ||
fullPattern = partial.concat($.map(repeatedPattern, function(v) { return v; })); | fullPattern = partial.concat($.map(repeatedPattern, function (v) { return v; })); | ||
} | } | ||
// Now insert start and end markers | // Now insert start and end markers | ||
var sCumulative = 0; | var sCumulative = 0; | ||
fullPattern = $.map(fullPattern, function(v) { | fullPattern = $.map(fullPattern, function (v) { | ||
// Don't bother appending if cumulative start time is outside range of interest | // Don't bother appending if cumulative start time is outside range of interest | ||
if ( sCumulative >= fillDuration ) { | if (sCumulative >= fillDuration) { | ||
return | return | ||
} | } | ||
Zeile 1.084: | Zeile 1.098: | ||
var fullMetas = {}; | var fullMetas = {}; | ||
$.each(eventData, function(k, v) { | $.each(eventData, function (k, v) { | ||
// Don't bother calculating if the meta hasn't been requested | // Don't bother calculating if the meta hasn't been requested | ||
if ( metaSequence.indexOf(k) == -1 ) { | if (metaSequence.indexOf(k) == -1) { | ||
return | return | ||
} | } | ||
Zeile 1.104: | Zeile 1.118: | ||
// Distance in percent of the 135 minute window (2 hour + 15 mins) | // Distance in percent of the 135 minute window (2 hour + 15 mins) | ||
var currentStartHourUTC = hour; | var currentStartHourUTC = hour; | ||
var percentOfTwoHours = ((minute / 60) * 50 ) * (120 / 135); | var percentOfTwoHours = ((minute / 60) * 50) * (120 / 135); | ||
if (useEvenHourStart === true) { | if (useEvenHourStart === true) { | ||
currentStartHourUTC = Math.floor(hour / 2) * 2; | currentStartHourUTC = Math.floor(hour / 2) * 2; | ||
percentOfTwoHours = (((hour % 2) + (minute / 60)) * 50 ) * (120 / 135); | percentOfTwoHours = (((hour % 2) + (minute / 60)) * 50) * (120 / 135); | ||
} | } | ||
Zeile 1.114: | Zeile 1.128: | ||
// Check if pointer has gone beyond the 1 or 2 hour mark, it will have slid to the left, in which case we need to redraw everything else too. | // Check if pointer has gone beyond the 1 or 2 hour mark, it will have slid to the left, in which case we need to redraw everything else too. | ||
if (startHourUTC != hour ) { | if (startHourUTC != hour) { | ||
// Erase existing event bars | // Erase existing event bars | ||
$('#event-container').html(''); | $('#event-container').html(''); | ||
Zeile 1.132: | Zeile 1.146: | ||
} else { | } else { | ||
// For positive timezones, add a plus sign before the hour offset. Negative timezones already have a minus sign. | // For positive timezones, add a plus sign before the hour offset. Negative timezones already have a minus sign. | ||
timezoneOffsetString = 'UTC' + (timezoneOffset < 0 ? timezoneOffset/60 : '+' + timezoneOffset/60); | timezoneOffsetString = 'UTC' + (timezoneOffset < 0 ? timezoneOffset / 60 : '+' + timezoneOffset / 60); | ||
if (twelveHourTimes == false) { | if (twelveHourTimes == false) { | ||
$('.event-pointer span').text(pad(now.getHours()) + ':' + pad(now.getMinutes()) + ' ' + timezoneOffsetString ); | $('.event-pointer span').text(pad(now.getHours()) + ':' + pad(now.getMinutes()) + ' ' + timezoneOffsetString); | ||
} else { | } else { | ||
$('.event-pointer span').text((((now.getHours() + 11) % 12) + 1) + ':' + pad(now.getMinutes()) + ' ' + (now.getHours() >= 12 ? 'PM' : 'AM')); | $('.event-pointer span').text((((now.getHours() + 11) % 12) + 1) + ':' + pad(now.getMinutes()) + ' ' + (now.getHours() >= 12 ? 'PM' : 'AM')); | ||
Zeile 1.153: | Zeile 1.167: | ||
$('.event-limit-text').css('cursor', 'pointer'); | $('.event-limit-text').css('cursor', 'pointer'); | ||
$('.event-limit-text.next').prop('title', uitext.timeshiftnexthoverpause); | $('.event-limit-text.next').prop('title', uitext.timeshiftnexthoverpause); | ||
$('.event-limit-text').click(function(e) { | $('.event-limit-text').click(function (e) { | ||
$('.event-pointer').css('left', '0%'); | $('.event-pointer').css('left', '0%'); | ||
$('.event-pointer-time').text(uitext.timeshiftresume); | $('.event-pointer-time').text(uitext.timeshiftresume); | ||
Zeile 1.171: | Zeile 1.185: | ||
// Check if its gone beyond midnight | // Check if its gone beyond midnight | ||
if (otherHourOffset + startHourUTC >= 24){ | if (otherHourOffset + startHourUTC >= 24) { | ||
otherHourOffset = otherHourOffset - 24; | otherHourOffset = otherHourOffset - 24; | ||
} | } | ||
Zeile 1.183: | Zeile 1.197: | ||
}); | }); | ||
// Restart live updating after clicking on the red marker | // Restart live updating after clicking on the red marker | ||
$('.event-pointer-time').click(function() { | $('.event-pointer-time').click(function () { | ||
mainEventTimer(true); | mainEventTimer(true); | ||
}); | }); | ||
} | } | ||
// Utility function #14: Refit compact timer to window width on resize. Only visible with the "Compact view" checkbox ticked. | // Utility function #14: Refit compact timer to window width on resize. Only visible with the "Compact view" checkbox ticked. H4 headings 220px to left | ||
function fitTimerToWindowWidth() { | function fitTimerToWindowWidth() { | ||
var w = $('#mw-content-text')[0].offsetWidth; | var w = $('#mw-content-text')[0].offsetWidth; | ||
$('#EventTimerCSS3').text( '#event-wrapper.compact { width: ' + (w - 220) + 'px } '); | $('#EventTimerCSS3').text('#event-wrapper.compact { width: ' + (w - (220 + 20)) + 'px } '); | ||
}; | }; | ||
// Utility function #15: Create wiki like links; inactive when on the same page as linked to. | |||
var pageTitlePattern = /(?:(?:\/wiki\/)(.*?)(?:\?|#|$)|(?:title=)(.*?)(?:&|#|$))/; | |||
function wikiLink(pageName, text) { | |||
text = text || pageName.replace(/_/g, " "); | |||
pageName = pageName.replace(/ /g, "_"); | |||
var match = pageTitlePattern.exec(location.href); | |||
var current = match[1] || match[2]; | |||
if (current === pageName) { | |||
return $(document.createElement("a")).attr("class", "mw-selflink selflink").text(text); | |||
} else { | |||
return $(document.createElement("a")).attr("href", "/wiki/" + pageName).attr("title", pageName.replace(/_/g, " ")).text(text); | |||
} | |||
} | |||
// MAIN FUNCTION | // MAIN FUNCTION | ||
function mainEventTimer(reloaded, paused) { | function mainEventTimer(reloaded, paused) { | ||
// Collect parameter options if specified | // Collect parameter options if specified | ||
var | var zoneParameter = '<!--{$zone|default:""|escape:"javascript"}-->'; | ||
var excludeParameter = '<!--{$exclude|default:""|escape:"javascript"}-->'; | |||
// If the timer was reloaded via apply, or scrolled, reset event content and timers, otherwise its the first run and we need to create the preferences user interface. | // If the timer was reloaded via apply, or scrolled, reset event content and timers, otherwise its the first run and we need to create the preferences user interface. | ||
Zeile 1.208: | Zeile 1.236: | ||
clearInterval(setIntervalHandle); | clearInterval(setIntervalHandle); | ||
} else if (zoneParameter == '') { | } else if (zoneParameter == '') { | ||
// Display checkboxes | // Display checkboxes if showing every timer (probably on the Event timers page) | ||
eventTimerPreferences(); | eventTimerPreferences(); | ||
} | } | ||
Zeile 1.228: | Zeile 1.256: | ||
} | } | ||
// Respect preferences if given and zone parameter | // Respect preferences if given and the zone parameter is specified | ||
var metaSequence = getEventTimerPreferences('sequence', defaultSequence); | var metaSequence = getEventTimerPreferences('sequence', defaultSequence); | ||
if (zoneParameter !== '') { | if (zoneParameter !== '') { | ||
// Zone parameter is set | |||
// Validate zone inputs exist in the full list | |||
var whitelist = Object.keys(eventData); | |||
var zones = []; | |||
$.each(zoneParameter.replace(', ', ',').split(','), function (i, v) { | |||
if (whitelist.indexOf(v) !== -1) { | |||
zones.push(v); | |||
} | |||
}); | |||
// Check if there are no valid options remaining | |||
if (zones.length == 0) { | |||
console.log('Error - No valid options provided within the zone parameter (' + zoneParameter + ')'); | |||
return; | |||
} | |||
// Check successful, continue - overwrite metaSequence | |||
$('#event-wrapper').addClass('zone'); | $('#event-wrapper').addClass('zone'); | ||
hideCategories = true; | hideCategories = true; | ||
Zeile 1.238: | Zeile 1.284: | ||
hideChatLinks = false; | hideChatLinks = false; | ||
metaSequence = []; | metaSequence = []; | ||
$.each( | $.each(zones, function (i, v) { | ||
metaSequence. | metaSequence.push(v); | ||
}); | |||
} else { | |||
// Zone parameter is blank | |||
// Exclusions | |||
$.each(excludeParameter.replace(', ', ',').split(','), function (i, v) { | |||
var index = metaSequence.indexOf(v); | |||
if (index !== -1) { | |||
metaSequence.splice(index, 1); | |||
} | |||
}); | }); | ||
// Check if there are no valid options remaining | |||
if (metaSequence.length == 0) { | |||
console.log('Error - Exclusions resulted in no valid options being provided (' + excludeParameter + ')'); | |||
return; | |||
} | |||
} | } | ||
Zeile 1.288: | Zeile 1.349: | ||
// DEFER LOADING SCRIPT UNTIL JQUERY IS READY. WAIT 40MS BETWEEN ATTEMPTS. | // DEFER LOADING SCRIPT UNTIL JQUERY IS READY. WAIT 40MS BETWEEN ATTEMPTS. | ||
function defer(method) { | function defer(method) { | ||
if (window.jQuery) { | |||
method(); | |||
} else { | |||
setTimeout(function () { defer(method) }, 40); | |||
} | |||
} | } | ||
Zeile 1.301: | Zeile 1.362: | ||
// Load the event timer after loading the jquery ui module | // Load the event timer after loading the jquery ui module | ||
$.ajaxSetup({ cache: true }); | $.ajaxSetup({ cache: true }); | ||
$.getScript('/index.php?title=Widget:Event-Timer/jquery_ui_sortable_min.js&action=raw&ctype=text/javascript', function( data, textStatus, jqxhr ) { | $.getScript('/index.php?title=Widget:Event-Timer/jquery_ui_sortable_min.js&action=raw&ctype=text/javascript', function (data, textStatus, jqxhr) { | ||
// Load the main widget from above | // Load the main widget from above | ||
mainEventTimer(); | mainEventTimer(); |
Version vom 8. Januar 2024, 13:22 Uhr
/*<nowiki>*/ /* Guild Wars 2 Wiki: Event timer */ // Increment this every time a release is added to invalidate the existing sequence and force users to load the new map timer. var version = 'v3.7.0'; // November 2023: Added logic path to reset sequence if all non-toptime event bars have been removed using the [x]. // GLOBAL VARIABLES // User interface buttons, labels, checkboxes var uitext = { widgetlink: "Widget:Event timer", widgetlinktext: "Dokumentation", timezonehover: "Dies ist deine Zeitzone", timeshiftresume: "Liveupdate aus - Hier klicken zum Aktivieren", timeshiftnexthoverpause: "Klicken um Liveupdate zu pausieren und zwei Stunden weiter zu gehen", timeshiftprevhover: "Klicken um zu den vorherigen zwei Stunden zu gehen", timeshiftnexthover: "Klicken um zu den folgenden zwei Stunden zu gehen", checkboxhover: "Klicke auf Anwenden um die Einstellungen zu speichern.", legendname: "Einstellungen für Event-Timer", applysettings: "Einstellungen anwenden", forgetsettings: "Einstellungen zurücksetzen", deleterowhover: "Versteckt diese Zeile. Setze die Einstellungen zurück, um alle Zeilen wieder anzuzeigen.", checkboxes: { twelvehour: { name: "12-Stunden-Uhrzeit anzeigen.", hover: "Wenn ausgewählt, werden Uhrzeiten im 12-Stunden-Format mit AM/PM angezeigt.", defaultvalue: false }, toptimes: { name: "Kompakte Darstellung.", hover: "Wenn ausgewählt, wird der Timer kompakter dargestellt.", defaultvalue: true }, compact: { name: "Kompakte Überschriftendarstellung.", hover: "Wenn ausgewählt, wird der Timer kompakter dargestellt, da die Überschrften links statt über dem Timer erscheinen. Keinen Effekt wenn Überschriften versteckt wurden.", defaultvalue: true }, hidecategories: { name: "Kategorien verstecken.", hover: "Wenn ausgewählt, wird der Timer durch das Entfernen der Kategorienüberschriften kompakter dargestellt.", defaultvalue: false }, hideheadings: { name: "Überschriften verstecken.", hover: "Wenn ausgewählt, wird der Timer durch das Entfernen der Karten-Meta-Überschriften kompakter dargestellt.", defaultvalue: false }, hidechatlinks: { name: "Chatlinks verstecken.", hover: "Wenn ausgewählt, werden keine Chatlinks angezeigt.", defaultvalue: false }, even: { name: "Nur zu geraden UTC-Stunden starten.", hover: "Wenn ausgewählt, beginnt der Timer immer zur vorherigen geraden UTC-Stunde.", defaultvalue: false } } }; // Event names, schedules, colours var eventData = { // Time t: { name: "", segments: { 0: { name: "", bg: "transparent" } }, sequences: { partial: [], pattern: [{r:0,d:15}] } }, // ** Zentraltyria ** dn: { category: "Zentraltyria", name: "Tageszeit", segments: { 1: { name: "Tag", link: "Tageszeit", bg: [255,255,255] }, 2: { name: "Dämmerung", link: "Tageszeit", bg: [[255,255,255],[122,134,171]] }, 3: { name: "Nacht", link: "Tageszeit", bg: [122,134,171] }, 4: { name: "Morgengrauen", link: "Tageszeit", bg: [[122,134,171],[255,255,255]] } }, sequences: { partial: [{r:3,d:25},{r:4,d:5}], pattern: [{r:1,d:70},{r:2,d:5},{r:3,d:40},{r:4,d:5}] } }, wb: { category: "Zentraltyria", name: "Welt-Bosse", link: "Welt-Boss", segments: { 1: { name: "Admiral Taidha Covington", link: "Die Kampagne gegen Taidha Covington", chatlink: "[&BKgBAAA=]", bg: [ 66,200,215] }, 2: { name: "Klaue von Jormag", link: "Zerstörung der Klaue Jormags", chatlink: "[&BHoCAAA=]", bg: [ 66,200,215] }, 3: { name: "Feuer-Elementar", link: "Thaumanova-Reaktor-Fallout", chatlink: "[&BEcAAAA=]", bg: [138,234,244] }, 4: { name: "Inquestur-Golem Typ II", link: "Besiegt den Inquestur-Golem Typ II", chatlink: "[&BNQCAAA=]", bg: [ 66,200,215] }, 5: { name: "Großer Dschungelwurm", link: "Bezwingt den großen Dschungelwurm", chatlink: "[&BEEFAAA=]", bg: [138,234,244] }, 6: { name: "Mega-Zerstörer", link: "Die Schlacht um den Mahlstromgipfel", chatlink: "[&BM0CAAA=]", bg: [ 66,200,215] }, 7: { name: "Modniir Ulgoth", link: "Bezwingt_Ulgoth_den_Modniir_und_seine_Diener", chatlink: "[&BLAAAAA=]", bg: [ 66,200,215] }, 8: { name: "Schatten-Behemoth", link: "Geheimnisse im Sumpf", chatlink: "[&BPcAAAA=]", bg: [138,234,244] }, 9: { name: "Svanir-Schamane", link: "Der gefrorene Schlund", chatlink: "[&BMIDAAA=]", bg: [138,234,244] }, 10: { name: "Der Zerschmetterer", link: "Kralkatorriks Vermächtnis", chatlink: "[&BE4DAAA=]", bg: [ 66,200,215] } }, sequences: { partial: [], pattern: [{r:1,d:15},{r:9,d:15},{r:6,d:15},{r:3,d:15},{r:10,d:15},{r:5,d:15},{r:7,d:15},{r:8,d:15},{r:4,d:15},{r:9,d:15},{r:2,d:15},{r:3,d:15},{r:1,d:15},{r:5,d:15},{r:6,d:15},{r:8,d:15},{r:10,d:15},{r:9,d:15},{r:7,d:15},{r:3,d:15},{r:4,d:15},{r:5,d:15},{r:2,d:15},{r:8,d:15}] } }, hwb: { category: "Zentraltyria", name: "Hardcore Welt-Bosse", link: "Welt-Boss", segments: { 0: { name: "", bg: [138,234,244] }, 1: { name: "Dreifacher Ärger", link: "Dreifacher Ärger", chatlink: "[&BKoBAAA=]", bg: [ 66,200,215] }, 2: { name: "Karka-Königin", link: "Inselkontrolle", chatlink: "[&BNUGAAA=]", bg: [ 66,200,215] }, 3: { name: "Tequatl der Sonnenlose", link: "Besiegt Tequatl den Sonnenlosen", chatlink: "[&BNABAAA=]", bg: [ 66,200,215] } }, sequences: { partial: [{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:30},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:90},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:120},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:120},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:30},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:150},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30}], pattern: [] } }, la: { category: "Zentraltyria", name: "Ley-Linien-Anomalie", link: "Legende Ley-Linien-Anomalie", segments: { 0: { name: "", bg: [251,132,152] }, 1: { name: "Baumgrenzen-Fälle", link: "Besiegt_die_Ley-Linien-Anomalie,_um_ihre_zerstörerische_Energie_aufzulösen,_bevor_sie_überlädt_(Baumgrenzen-Fälle)", chatlink: "[&BEwCAAA=]", bg: [215, 66, 91] }, 2: { name: "Eisenmark", link: "Besiegt_die_Ley-Linien-Anomalie,_um_ihre_zerstörerische_Energie_aufzulösen,_bevor_sie_überlädt_(Eisenmark)", chatlink: "[&BOYBAAA=]", bg: [215, 66, 91] }, 3: { name: "Gendarran-Felder", link: "Besiegt_die_Ley-Linien-Anomalie,_um_ihre_zerstörerische_Energie_aufzulösen,_bevor_sie_überlädt_(Gendarran-Felder)", chatlink: "[&BO0AAAA=]", bg: [215, 66, 91] } }, sequences: { partial: [{r:0,d:20},{r:1,d:20},{r:0,d:100},{r:2,d:20},{r:0,d:100},{r:3,d:20}], pattern: [{r:0,d:100},{r:1,d:20},{r:0,d:100},{r:2,d:20},{r:0,d:100},{r:3,d:20}] } }, pvpat: { category: "Zentraltyria", name: "PvP-Turniere", link: "Automatisierte Turniere", segments: { 0: { name: "", bg: [251,132,152] }, 1: { name: "Automatisiertes Turnier: Balthasars Rauferei", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] }, 2: { name: "Automatisiertes Turnier: Grenths Gastspiel", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] }, 3: { name: "Automatisiertes Turnier: Melandrus Begegnung", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] }, 4: { name: "Automatisiertes Turnier: Lyssas Legionen", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] } }, sequences: { partial: [], pattern: [{r:1,d:60},{r:0,d:120},{r:2,d:60},{r:0,d:120},{r:3,d:60},{r:0,d:120},{r:4,d:60},{r:0,d:120}] } }, eotn: { category: "Lebendige Welt Staffel 1", name: "Auge des Nordens", link: "Auge des Nordens", segments: { 0: { name: "", bg: [251,132,152] }, 1: { name: "Die Verdrehte Marionette (Öffentlich)", link: "Die Verdrehte Marionette", chatlink: "[&BAkMAAA=]", bg: [234, 98,121] }, 2: { name: "Tower of Nightmares (Public)", link: "The Tower of Nightmares (meta event)", chatlink: "[&BAkMAAA=]", bg: [234, 98,121] }, 3: { name: "Battle For Lion's Arch (Public)", link: "The Battle For Lion's Arch", chatlink: "[&BAkMAAA=]", bg: [234, 98,121] } }, sequences: { partial: [], pattern: [{r:1,d:20},{r:0,d:10},{r:3,d:15},{r:0,d:45},{r:2,d:15},{r:0,d:15}] } }, si: { category: "Lebendige Welt Staffel 1", name: "Scarlets Invasion", link: "Besiegt die eindringenden Diener von Scarlet Dornstrauch", segments: { 0: { name: "", bg: [251,132,152] }, 1: { name: "Besiegt Scarlets Diener", link: "Besiegt die eindringenden Diener von Scarlet Dornstrauch", chatlink: "[&BOQAAAA=]", bg: [234, 98,121] } }, sequences: { partial: [{r:0,d:60},{r:1,d:15}], pattern: [{r:0,d:105},{r:1,d:15}] } }, // ** Lebendige Welt Staffel 2 ** dt: { category: "Lebendige Welt Staffel 2", name: "Trockenkuppe", segments: { 1: { name: "Absturzstelle", bg: [251,227,132] }, 2: { name: "Sandsturm!", chatlink: "[&BIAHAAA=]", bg: [215,185, 66] } }, sequences: { partial: [], pattern: [{r:1,d:40},{r:2,d:20}] } }, // ** Heart of Thorns ** vb: { category: "Heart of Thorns", name: "Grasgrüne Schwelle", segments: { 1: { name: "Sicherung der Grasgrünen Schwelle", link: "Grasgrüne Schwelle#Tagsüber", bg: [231,251,132] }, 2: { name: "Die Nacht und der Feind", bg: [211,234, 98] }, 3: { name: "Nachtbosse", link: "Die Nacht und der Feind", chatlink: "[&BAgIAAA=]", bg: [190,215, 66] } }, sequences: { partial: [{r:2,d:10},{r:3,d:20}], pattern: [{r:1,d:75},{r:2,d:25},{r:3,d:20}] } }, ab: { category: "Heart of Thorns", name: "Güldener Talkessel", segments: { 1: { name: "Pylonen", link: "Die Verteidigung von Tarir", chatlink: "[&BN0HAAA=]", bg: [231,251,132] }, 2: { name: "Herausforderungen", link: "Feuerprobe", chatlink: "[&BGwIAAA=]", bg: [211,234, 98] }, 3: { name: "Rankenkrake", link: "Schlacht in Tarir", chatlink: "[&BAIIAAA=]", bg: [190,215, 66] }, 4: { name: "Reset", link: "Eine kurze Verschnaufpause", bg: [211,234, 98] } }, sequences: { partial: [{r:1,d:45},{r:2,d:15},{r:3,d:20},{r:4,d:10}], pattern: [{r:1,d:75},{r:2,d:15},{r:3,d:20},{r:4,d:10}] } }, td: { category: "Heart of Thorns", name: "Verschlungene Tiefen", segments: { 1: { name: "Helft den Außenposten", link: "Verschlungene_Tiefen#Meta-Events", bg: [231,251,132] }, 2: { name: "Vorbereitung", link: "König des Dschungels", bg: [211,234, 98] }, 3: { name: "Chak-Potentat", link: "König des Dschungels", chatlink: "[&BPUHAAA=]", bg: [190,215, 66] } }, sequences: { partial: [{r:1,d:25},{r:2,d:5},{r:3,d:20}], pattern: [{r:1,d:95},{r:2,d:5},{r:3,d:20}] } }, ds: { category: "Heart of Thorns", name: "Widerstand des Drachen", segments: { 1: { name: "Start des Angriffs", link: "Widerstand des Drachen (Meta-Event)", chatlink: "[&BBAIAAA=]", bg: [190,215, 66] }, 2: { name: "(fortgesetzt)", link: "Widerstand des Drachen (Meta-Event)", bg: [190,215, 66] } }, sequences: { partial: [{r:2,d:90}], pattern: [{r:1,d:120}] } }, // ** Lebendige Welt Staffel 3 ** ld: { category: "Lebendige Welt Staffel 3", name: "Doric-See", segments: { 1: { name: "Saidras Hafen", link: "Kontrolle des Weißen Mantels: Saidras Hafen", chatlink: "[&BK0JAAA=]", bg: [251,132,152] }, 2: { name: "Neulehmwald", link: "Kontrolle des Weißen Mantels: Neulehmwald", chatlink: "[&BLQJAAA=]", bg: [234, 98,121] }, 3: { name: "Norans Heimstatt", link: "Kontrolle des Weißen Mantels: Norans Heimstatt", chatlink: "[&BK8JAAA=]", bg: [215, 66, 91] } }, sequences: { partial: [{r:2,d:30}], pattern: [{r:3,d:30},{r:1,d:45},{r:2,d:45}] } }, // ** Path of Fire ** co: { category: "Path of Fire", name: "Kristalloase", segments: { 0: { name: "", bg: [251,199,132] }, 1: { name: "Runde 1 bis 3", link: "Kasino-Blitz", chatlink: "[&BLsKAAA=]", bg: [234,175, 98] }, 2: { name: "Piñata/Reset", link: "Kasino-Blitz", chatlink: "[&BLsKAAA=]", bg: [215,150, 66] } }, sequences: { partial: [{r:0,d:5},{r:1,d:16},{r:2,d:9}], pattern: [{r:0,d:95},{r:1,d:16},{r:2,d:9}] } }, dh: { category: "Path of Fire", name: "Wüsten-Hochland", segments: { 0: { name: "", bg: [251,199,132] }, 1: { name: "Vergrabene Schätze", link: "Die Suche nach vergrabenen Schätzen", chatlink: "[&BGsKAAA=]", bg: [234,175, 98] } }, sequences: { partial: [{r:0,d:60},{r:1,d:20}], pattern: [{r:0,d:100},{r:1,d:20}] } }, er: { category: "Path of Fire", name: "Elon-Flusslande", segments: { 0: { name: "", bg: [251,199,132] }, 1: { name: "Fels der Weissagung", link: "Der Pfad zum Aufstieg", chatlink: "[&BFMKAAA=]", bg: [234,175, 98] }, 2: { name: "Doppelgänger", link: "Der Pfad zum Aufstieg", chatlink: "[&BCgKAAA=]", bg: [215,150, 66] } }, sequences: { partial: [{r:2,d:15}], pattern: [{r:0,d:75},{r:1,d:25},{r:2,d:20}] } }, de: { category: "Path of Fire", name: "Das Ödland", segments: { 0: { name: "", bg: [251,199,132] }, 1: { name: "Schlünde der Qual", chatlink: "[&BKMKAAA=]", bg: [215,150, 66] }, 2: { name: "Aufstand der Junundu", chatlink: "[&BMEKAAA=]", bg: [234,175, 98] } }, sequences: { partial: [{r:0,d:30},{r:2,d:20},{r:0,d:10}], pattern: [{r:1,d:20},{r:0,d:10},{r:2,d:20},{r:0,d:40},{r:2,d:20},{r:0,d:10}] } }, dv: { category: "Path of Fire", name: "Domäne Vaabi", segments: { 0: { name: "", bg: [251,199,132] }, 1: { name: "Zorn der Schlangen", chatlink: "[&BHQKAAA=]", bg: [234,175, 98] }, 2: { name: "Im Feuer geschmiedet", chatlink: "[&BO0KAAA=]", bg: [215,150, 66] } }, sequences: { partial: [{r:2,d:30}], pattern: [{r:1,d:30},{r:2,d:30},{r:0,d:30},{r:2,d:30}] } }, // ** Lebendige Welt Staffel 4 ** ai: { category: "Lebendige Welt Staffel 4", name: "Eindringende Erweckte", segments: { 0: { name: "", bg: [187,119,207] }, 1: { name: "Eindringende Erweckte", link: "Besiegt die eindringenden Erweckten", bg: [157,65,185] }, }, sequences: { partial: [{r:0,d:30}], pattern: [{r:1,d:15},{r:0,d:45}] } }, di: { category: "Lebendige Welt Staffel 4", name: "Domäne Istan", segments: { 0: { name: "", bg: [187,119,207] }, 1: { name: "Palawadan", link: "Palawadan, Juwel von Istan (Meta-Event)", chatlink: "[&BAkLAAA=]", bg: [157,65,185] }, }, sequences: { partial: [{r:1,d:15}], pattern: [{r:0,d:90},{r:1,d:30}] } }, jb: { category: "Lebendige Welt Staffel 4", name: "Jahai-Klippen", segments: { 0: { name: "", bg: [187,119,207] }, 1: { name: "Eskorte", link: "Eskortiert die DERV zu den Gräben des Zerschmetterers", chatlink: "[&BIMLAAA=]", bg: [175, 96,199] }, 2: { name: "Zerschmetterer", link: "Vernichtet den Todesgebrandmarkten Zerschmetterer", chatlink: "[&BJMLAAA=]", bg: [157,65,185] }, }, sequences: { partial: [{r:0,d:60},{r:1,d:15},{r:2,d:15}], pattern: [{r:0,d:90},{r:1,d:15},{r:2,d:15}] } }, tp: { category: "Lebendige Welt Staffel 4", name: "Donnerkopf-Gipfel", segments: { 0: { name: "", bg: [187,119,207] }, 1: { name: "Feste Donnerkopf", link: "Feste Donnerkopf (Meta-Event)", chatlink: "[&BLsLAAA=]", bg: [157,65,185] }, 2: { name: "Öl auf dem Eis", chatlink: "[&BKYLAAA=]", bg: [157,65,185] }, }, sequences: { partial: [{r:1,d:5},{r:0,d:40},{r:2,d:15}], pattern: [{r:0,d:45},{r:1,d:20},{r:0,d:40},{r:2,d:15}] } }, // ** The Icebrood Saga ** gv: { category: "Die Eisbrut-Saga", name: "Grothmar-Tal", segments: { 0: { name: "", bg: [132,201,251] }, 1: { name: "Flammenabbild", link: "Zeremonie der Heiligen Flamme", chatlink: "[&BA4MAAA=]", bg: [ 98,177,234] }, 2: { name: "Schrein", link: "Heimsuchung des Schreins des Schicksalwissens", chatlink: "[&BA4MAAA=]", bg: [ 66,153,215] }, 3: { name: "Schleimgrube", link: "Schleimgruben-Prüfungen", chatlink: "[&BPgLAAA=]", bg: [ 98,177,234] }, 4: { name: "Konzert", link: "Ein Konzert für die Ewigkeit", chatlink: "[&BPgLAAA=]", bg: [ 66,153,215] } }, sequences: { partial: [{r:0,d:10}], pattern: [{r:1,d:15},{r:0,d:13},{r:2,d:22},{r:0,d:5},{r:3,d:20},{r:0,d:15},{r:4,d:15},{r:0,d:15}] } }, bm: { category: "Die Eisbrut-Saga", name: "Bjora-Sümpfe", segments: { 0: { name: "", bg: [132,201,251] }, 1: { name: "Drakkar und die Geister der Wildnis", link: "Champion des Eisdrachen", chatlink: "[&BDkMAAA=]", bg: [ 66,153,215] }, 2: { name: "Rabenschreine", link: "Stürme des Winters", chatlink: "[&BCcMAAA=]", bg: [ 98,177,234] }, 3: { name: "Scherben und Konstrukt", link: "Stürme des Winters", chatlink: "[&BCcMAAA=]", bg: [ 66,153,215] }, 4: { name: "Eisbrut Champions", link: "Stürme des Winters", chatlink: "[&BCcMAAA=]", bg: [ 98,177,234] } }, sequences: { partial: [{r:3,d:5},{r:4,d:15}], pattern: [{r:0,d:45},{r:1,d:35},{r:0,d:5},{r:2,d:15},{r:3,d:5},{r:4,d:15}] } }, dsp: { category: "Die Eisbrut-Saga", name: "Drachensturm", segments: { 0: { name: "", bg: [132,201,251] }, 1: { name: "Drachensturm", link: "Drachensturm", chatlink: "[&BAkMAAA=]", bg: [ 66,153,215] } }, sequences: { partial: [{r:0,d:60}], pattern: [{r:1,d:20},{r:0,d:100}] } }, // ** End of Dragons ** cdn: { category: "End of Dragons", name: "Cantha: Tageszeit", segments: { 1: { name: "Tag", link: "Tageszeit", bg: [255,255,255] }, 2: { name: "Dämmerung", link: "Tageszeit", bg: [[255,255,255],[122,134,171]] }, 3: { name: "Nacht", link: "Tageszeit", bg: [122,134,171] }, 4: { name: "Morgengrauen", link: "Tageszeit", bg: [[122,134,171],[255,255,255]] } }, sequences: { partial: [{r:3,d:35},{r:4,d:5}], pattern: [{r:1,d:55},{r:2,d:5},{r:3,d:55},{r:4,d:5}] } }, sp: { category: "End of Dragons", name: "Provinz Seitung", segments: { 0: { name: "", bg: [195,255,245] }, 1: { name: "Ätherklingen-Angriff", chatlink: "[&BGUNAAA=]", bg: [90,243,222] } }, sequences: { partial: [{r:0,d:90}], pattern: [{r:1,d:30},{r:0,d:90}] } }, nkc: { category: "End of Dragons", name: "Stadt Neu-Kaineng", segments: { 0: { name: "", bg: [195,255,245] }, 1: { name: "Kaineng-Energieausfall", chatlink: "[&BBkNAAA=]", bg: [90,243,222] } }, sequences: { partial: [], pattern: [{r:1,d:30},{r:0,d:90}] } }, tew: { category: "End of Dragons", name: "Die Echowald-Wildnis", segments: { 0: { name: "", bg: [138,234,244] }, 1: { name: "Bandenkrieg", link: "Der Bandenkrieg von Echowald", chatlink: "[&BMwMAAA=]", bg: [ 66,200,215] }, 2: { name: "Espenwald", link: "Zerstört mithilfe der der Belagerungsschildkröten die Schildgeneratoren, während Ihr Euch durch das Fort kämpft", chatlink: "[&BPkMAAA=]", bg: [ 96,220,235] } }, sequences: { partial: [], pattern: [{r:0,d:30},{r:1,d:35},{r:0,d:35},{r:2,d:20}] } }, dre: { category: "End of Dragons", name: "Drachen-Ende", segments: { 1: { name: "Vorbereitungen", chatlink: "[&BKIMAAA=]", bg: [195,255,245] }, 2: { name: "Die Schlacht ums Jademeer", chatlink: "[&BKIMAAA=]", bg: [90,243,222] } }, sequences: { partial: [], pattern: [{r:1,d:60},{r:2,d:60}] } }, // ** Secrets of the Obscure ** sa: { category: "Secrets of the Obscure", name: "Himmelswacht-Archipel", segments: { 0: { name: "", bg: [250,206,133] }, 1: { name: "Den Turm des Zauberers entriegeln", link: "Den Turm des Zauberers entriegeln", chatlink: "[&BL4NAAA=]", bg: [210,155, 73] } }, sequences: { partial: [{r:0,d:60}], pattern: [{r:1,d:25},{r:0,d:95}] } }, wt: { category: "Secrets of the Obscure", name: "Der Turm des Zauberers", segments: { 0: { name: "", bg: [250,206,133] }, 1: { name: "Himmelsschuppen-Zielübungen", link: "Himmelsschuppen-Zielübungen im Turm des Zauberers", chatlink: "[&BB8OAAA=]", bg: [226,171, 73] }, 2: { name: "Nachtflug", link: "Turm des Zauberers: Nachtflug", chatlink: "[&BB8OAAA=]", bg: [226,171, 73] }, 3: { name: "Himmelsschuppen-Zielübungen & Nachtflug", link: "Abenteuer#Secrets of the Obscure", chatlink: "[&BB8OAAA=]", bg: [200,136, 54] } }, sequences: { partial: [{r:2,d:20},{r:0,d:40}], pattern: [{r:1,d:40},{r:3,d:15},{r:2,d:25},{r:0,d:40}] } }, am: { category: "Secrets of the Obscure", name: "Amnytas", segments: { 0: { name: "", bg: [250,206,133] }, 1: { name: "Verteidigung von Amnytas", link: "Die Verteidigung von Amnytas", chatlink: "[&BDQOAAA=]", bg: [210,155, 73] } }, sequences: { partial: [], pattern: [{r:1,d:25},{r:0,d:95}] } }, con: { category: "Secrets of the Obscure", name: "Konvergenz (Instanz)", segments: { 0: { name: "", bg: [250,206,133] }, 1: { name: "Konvergenzen", link: "Konvergenz (Instanz)", chatlink: "[&BB8OAAA=]", bg: [226,171, 73] } }, sequences: { partial: [{r:0,d:90}], pattern: [{r:1,d:10},{r:0,d:170}] } }, // ** Special Events ** lc: { category: "Spezialevents", name: "Labyrinthklippen", segments: { 0: { name: "", bg: [138,234,244] }, 1: { name: "Skiff-Rennen", link: "Labyrinth-Skiffe: Bald fängt ein Rennen an", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] }, 2: { name: "Schatzjagd", link: "Sammelt vor Ablauf der Zeit so viel Beute wie möglich!", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] }, 3: { name: "Schweberochen-Rennen", link: "Schwebe-Rochen-Slalom: Erreicht die Ziellinie", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] }, 4: { name: "Angeln", link: "Anmeldung zum Angelturnier", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] }, 5: { name: "Dolyak-Rennen", link: "Fliegender Dolyak: Erreicht die Ziellinie", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] } }, sequences: { partial: [], pattern: [{r:1,d:10},{r:0,d:20},{r:2,d:30},{r:0,d:15},{r:3,d:10},{r:0,d:5},{r:4,d:10},{r:0,d:5},{r:5,d:10},{r:0,d:5}] } }, db: { category: "Spezialevents", name: "Drachen-Gepolter", segments: { 0: { name: "", bg: [138,234,244] }, 1: { name: "Wanderer-Hügel", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BH0BAAA=]", bg: [ 66,200,215] }, 2: { name: "Schauflerschreck-Klippen", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BGMCAAA=]", bg: [ 66,200,215] }, 3: { name: "Lornars Pass", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BJkBAAA=]", bg: [ 66,200,215] }, 4: { name: "Schneekuhlenhöhen", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BL4AAAA=]", bg: [ 66,200,215] } }, sequences: { partial: [], pattern: [{r:1,d:5},{r:0,d:10},{r:2,d:5},{r:0,d:10},{r:3,d:5},{r:0,d:10},{r:4,d:5},{r:0,d:10}] } }, ha: { category: "Spezialevents", name: "Halloween", segments: { 0: { name: "", bg: [242,215,162] }, 1: { name: "Der Verrückte König sagt", link: "Der Verrückte König sagt:", chatlink: "[&BBAEAAA=]", bg: [232,163,31] } }, sequences: { partial: [], pattern: [{r:1,d:10},{r:0,d:110}] } } }; // Placeholder object which will become a copy of eventData, but only with the metas specified in defaultSequence. var customEventData = {}; // Sequence in which the elements will render. var defaultSequence = Object.keys(eventData); // If there are more than 10 elements showing, it's probably a long way between the first times and the last, so add another to the end. if (defaultSequence.length > 10) { defaultSequence.push('t'); } // Calculate the user timezone offset for continued later use. Globally track start hour too. var now = new Date(), timezoneOffset = (-1 * now.getTimezoneOffset()), startHourUTC, twelveHourTimes, setIntervalHandle, otherHourOffset = 0, usedHeadings = []; // UTILITY FUNCTIONS // Utility function #1: Write CSS function writeTimerCSS() { // ** Sheet 2 - Event colour scheme ** var cssText = $.map(eventData, function (metaEventData, metaKey) { var x; return $.map(metaEventData.segments, function (v, k) { x = ''; switch (typeof v.bg) { case 'object': switch (v.bg.length) { case 2: // linear-gradients x = '.event-bar-segment.' + metaKey + k + ' { background: linear-gradient(90deg, rgb(' + v.bg[0].join(',') + '), rgb(' + v.bg[1].join(',') + ')) }'; break; case 3: x = ['.event-bar-segment.' + metaKey + k + ' { background: rgb(' + v.bg.join(',') + ') }', '.event-bar-segment.' + metaKey + k + '.future { background: rgba(' + v.bg.join(',') + ',0.3) }']; break; } break; case 'string': // transparent or other alternative text x = '.event-bar-segment.' + metaKey + k + ' { background: ' + v.bg + '}'; break; } return x; }); }).join('\n'); $('#EventTimerCSS2').text('/* Widget:Event timer - Stylesheet 1 */\n' + cssText); // ** Sheet 3 - Compact window width ** // Run once fitTimerToWindowWidth(); // And rerun when the window changes size var resizeTimer; $(window).resize(function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(fitTimerToWindowWidth, 250); }); } // Utility function #2 and #3: HTML5 localStorage operator functions used to request existing preferences, and store user preferences for later visits function getEventTimerPreferences(keyname, defaultvalue) { var response = JSON.parse(localStorage.getItem('event-timer-' + keyname)); if (typeof response == 'undefined' || response == null) { response = defaultvalue; } return response; } function setEventTimerPreferences(keyname, value, defaultvalue) { switch (typeof value) { case 'string': case 'object': case 'boolean': break; default: console.log('Invalid preference ignored:', value); value = defaultvalue; break; } try { localStorage.removeItem('event-timer-' + keyname); localStorage.setItem('event-timer-' + keyname, JSON.stringify(value)); console.log('Changed preference: ', keyname); } catch (e) { console.log('localStorage not supported (HTML5 browser required)'); } } // Utility function #4: Create a legend with checkboxes for viewers to set their preferences. function eventTimerPreferences() { function addCheckbox(keyname, desc, hoverdesc, defaultvalue) { hoverdesc += ' >> ' + uitext.checkboxhover; var box = $(document.createElement("input")).attr("type", "checkbox").attr("id", keyname + "-toggle").attr("title", hoverdesc); var label = $(document.createElement("label")).attr("for", keyname + "-toggle").attr("title", hoverdesc).text(desc); box.attr('checked', getEventTimerPreferences(keyname, defaultvalue)); eventTimerSettings.append(box).append(label); } // Create fieldset container with legend var eventTimerSettings = $(document.createElement("fieldset")).attr("class", "widget").attr("id", "event-timer-legend") .append($(document.createElement("legend")).text(uitext.legendname)); $.each(uitext.checkboxes, function (k, v) { addCheckbox(k, v.name, v.hover, v.defaultvalue); }); eventTimerSettings.append($(document.createElement("input")).attr("id", "apply-button").attr("class", "mw-ui-button button").attr("type", "button").attr("value", uitext.applysettings)); eventTimerSettings.append($(document.createElement("input")).attr("id", "forget-button").attr("class", "mw-ui-button button").attr("type", "button").attr("value", uitext.forgetsettings)); eventTimerSettings.append($(document.createElement("span")) .append(wikiLink(uitext.widgetlink, uitext.widgetlinktext)) ); $('#event-wrapper').after(eventTimerSettings); // Save checkbox settings $.each(uitext.checkboxes, function (k, v) { $('#' + k + '-toggle').click(function () { setEventTimerPreferences(k, $('#' + k + '-toggle').prop('checked'), v.defaultvalue); }); }); $('#apply-button').click(function () { mainEventTimer(true); }); $('#forget-button').click(function () { try { // Remove local storage and reset checkboxes localStorage.removeItem('event-timer-version'); localStorage.removeItem('event-timer-sequence'); $.each(uitext.checkboxes, function (k, v) { localStorage.removeItem('event-timer-' + k); $('#' + k + '-toggle').prop('checked', v.defaultvalue); }); console.log('Removed stored event timer preferences.'); } catch (e) { console.log('localStorage not supported (HTML5 browser required)'); } mainEventTimer(true); }); } // Utility function #5: Convert a time given in minutes since 00:00 into a recognizable time. (1440 = one whole day, 1515 = one whole schedule day) function unwrapUTC(time) { var timeRaw, timeString; // Check combined offset in hours is not beyond 23:59 time = time % 1440; // Calculate the hours and minutes var hour = Math.floor(time / 60); var minute = time % 60; // If timezone offset is zero, use UTC time and don't bother with date objects, otherwise use local time if (timezoneOffset == 0) { if (twelveHourTimes == false) { timeRaw = pad(hour) + ':' + pad(minute); timeString = pad(hour) + ':' + pad(minute); } else { timeRaw = (((hour + 11) % 12) + 1) + ':' + pad(minute) + ' ' + (hour >= 12 ? 'PM' : 'AM'); timeString = (((hour + 11) % 12) + 1) + ':' + pad(minute) + ' ' + (hour >= 12 ? 'PM' : 'AM'); } } else { var date = new Date(); date.setUTCHours(hour, minute, 0, 0); if (twelveHourTimes == false) { timeRaw = pad(date.getHours()) + ':' + pad(date.getMinutes()); timeString = $(document.createElement("span")).attr("title", uitext.timezonehover + " (UTC" + (timezoneOffset < 0 ? timezoneOffset / 60 : "+" + timezoneOffset / 60) + ")").text(pad(date.getHours()) + ':' + pad(date.getMinutes())); } else { timeRaw = (((date.getHours() + 11) % 12) + 1) + ':' + pad(date.getMinutes()) + ' ' + (date.getHours() >= 12 ? 'PM' : 'AM'); timeString = $(document.createElement("span")).attr("title", uitext.timezonehover + " (UTC" + (timezoneOffset < 0 ? timezoneOffset / 60 : "+" + timezoneOffset / 60) + ")").text((((date.getHours() + 11) % 12) + 1) + ":" + pad(date.getMinutes()) + " " + (date.getHours() >= 12 ? "PM" : "AM")); } } return { raw: timeRaw, string: timeString }; } // Utility function #6: Zero pad numbers into strings of character length two. function pad(s) { return (s < 10 ? '0' : '') + s; } // Utility function #7: Create a one-click select element for a chatlink. function chatLinkSelect(chatLinkCode) { var span = document.createElement('span'); span.innerHTML = chatLinkCode; var input = document.createElement('input'); input.className = 'chatlink'; input.type = 'text'; input.value = chatLinkCode; input.readOnly = true; input.spellcheck = false; $(span).click(function () { this.style.visibility = 'hidden'; input.style.display = 'inline-block'; input.focus(); input.select(); }); $(input).blur(function () { this.style.display = null; span.style.visibility = null; }); var output = document.createElement('span'); output.className = 'event-chatlink'; output.appendChild(input); output.appendChild(span); return output; } // Utility function #8: Draw blocks for the given object function drawRow(metaKey, metaSingular) { // Create a bar container (this will hold the bar and the associated title) var barcontainer = $(document.createElement("div")).attr("class", "event-bar-container " + metaKey).attr("data-abbr", metaKey); // Display category if not used before if (typeof metaSingular.category != 'undefined' && usedHeadings.indexOf(metaSingular.category) == -1) { usedHeadings.push(metaSingular.category); barcontainer.append($(document.createElement("h3")).attr("class", metaKey).text(metaSingular.category)); } // Display heading with link always if (typeof metaSingular.link != 'undefined') { barcontainer.append($(document.createElement("h4")) .append($(document.createElement("span")) .append(wikiLink(metaSingular.link, metaSingular.name)) ) ); } else { barcontainer.append($(document.createElement("h4")) .append($(document.createElement("span")) .append(wikiLink(metaSingular.name)) ) ); } // Create a bar for the meta segments var bar = $(document.createElement("div")).attr("class", "event-bar"); // For each event create a "segment" $.each(metaSingular.sequences.refined, function (i, v) { var name = metaSingular.segments[v.r].name, link = metaSingular.segments[v.r].link || (name == '' ? '' : name), chatlink = metaSingular.segments[v.r].chatlink || ''; var time = unwrapUTC(v['s']); // Create a segment to represent that phase, and set the width based on the duration var segment = $(document.createElement("div")).attr("class", "event-bar-segment " + metaKey + v.r + v.cl).css("width", (100 * v["d"] / 135) + "%").attr("title", (metaSingular.name ? metaSingular.name + "\r" : "") + time.raw + (name == "" ? "" : " - ") + name); // Time var segmentTime = $(document.createElement("span")).attr("class", "event-time") .append(time.string); segment.append(segmentTime); // Segment event name and link // Use NBSP instead of leaving empty event names, as if the name is blank for a whole 2hr15 slot, then the segment height will reduce if the span element is empty if (name == "") { name = "\u00a0"; } segment.append($(document.createElement("span")).attr("class", "event-name") .append(link == "" ? document.createTextNode(name) : wikiLink(link, name))); // Chatlink if (chatlink != '') { segment.append(chatLinkSelect(chatlink)); } bar.append(segment); }); bar.append($(document.createElement("span")).attr("class", "event-bar-exit").attr("title", uitext.deleterowhover).text("[X]")); barcontainer.append(bar); $('#event-container').append(barcontainer); } // Utility function #9: Refine the schedule from 1515 to 135 minutes. Firstly apply a rough filter around the window, then truncate to ensure events are within the window. function filterEventData(metas) { function refineRow(schedule, metaKey) { // Window start, future and end times in minutes var ws = startHourUTC * 60; var wf = ws + 120; var we = ws + 135; // Filter the data down from 24 hours to roughly 2 hours. function timeWithinWindow(schedule) { return ((schedule.e > ws && schedule.s < we)); } var roughSchedule = schedule.filter(timeWithinWindow); // Refine the data to restrict lengths to visible window var refinedSchedule = []; $.each(roughSchedule, function (i, v) { // Local copies that we can adjust var r = v.r, s = v.s, e = v.e; // Check if window starts after the segment started, if so, crop it if (ws > s) { s = ws; if (metaKey == 'ds' && r == 1) { r = 2; // Special case: Dragon's Stand } } // Check end of segment is before window end, if not, crop it if (e > we) { e = we; } // Check if segment crosses the 2 hour marker, if it does, split into two if (s < wf && wf < e) { // Two objects, one beginning to the left of the future line + ending at the future line, and one starting at the future line refinedSchedule.push({ r: r, // Reference id, e.g. wb1 s: s, // Start minutes, e.g. 10 e: wf, // End minutes, e.g. 60 d: wf - s, // Duration, e.g. 50 cl: '' // Class placeholder only used for future last 15 minutes segments }); if (metaKey == 'ds' && r == 1) { r = 2; // Special case: Dragon's Stand future } refinedSchedule.push({ r: r, s: wf, e: e, d: e - wf, cl: ' future' }); } else if (wf < e) { // Just one object, with the ending after the future line + beginning on or after future line refinedSchedule.push({ r: r, s: s, e: e, d: e - s, cl: ' future' }); } else { // Just one object, with the ending on or before the future line refinedSchedule.push({ r: r, s: s, e: e, d: e - s, cl: '' }); } }); return refinedSchedule; } // Refine schedule to fit 135 minute view. $.each(metas, function (k, v) { metas[k].sequences.refined = refineRow(v.sequences.full, k); }); return metas; } // Utility function #10: Draw meta event "phases" as segments within map "bars" for each meta. function createEventBars(useEvenHourStart, metaSequence, otherHourOffset) { // All event bars and segments need to be created with the same start time var now = new Date(); startHourUTC = now.getUTCHours(); // Check if otherHour specified if (otherHourOffset) { startHourUTC += otherHourOffset; } // Use even hours if required, or any hour if not specified if (useEvenHourStart === true) { startHourUTC = Math.floor(startHourUTC / 2) * 2; } // Filter the schedule for the current 135 minute window customEventData = filterEventData(customEventData); // Reset any previously set category heading tracking usedHeadings = []; // Do the work $.each(metaSequence, function (i, metaKey) { drawRow(metaKey, customEventData[metaKey]); }); // Allow reordering of elements $('#event-container').sortable({ placeholder: 'ui-sortable-placeholder', update: function () { // Update stored values var eventBars = $('.event-bar-container'); var eventAbbrs = []; $.each(eventBars, function () { eventAbbrs.push(this.getAttribute('data-abbr')); }); setEventTimerPreferences('sequence', eventAbbrs, defaultSequence); console.log('Rearranged sequence to: ' + JSON.stringify(eventAbbrs)); // Now reload otherwise people whine about category titles. mainEventTimer(true); } }); // Allow closure of event bars $('.event-bar-exit').click(function () { var eventBar = this.closest('.event-bar-container'); var eventAbbr = eventBar.getAttribute('data-abbr'); var currentPref = getEventTimerPreferences('sequence', defaultSequence); // Adjust stored preferences to remove given element from preferences var abbrIndex = currentPref.indexOf(eventAbbr); if (abbrIndex > -1) { currentPref.splice(abbrIndex, 1); } setEventTimerPreferences('sequence', currentPref, defaultSequence); console.log('Deleted element. Remaining sequence: ' + JSON.stringify(currentPref)); // And hide chosen element whilst still on this page. Next time it won't load that element until you press reset. // $('[data-abbr="'+eventAbbr+'"]').remove(); -- not required if we redraw // Check if remaining sequence is only length 2 (i.e. only the the top and bottom times remain - everything else deleted) // If so, reset the sequence entirely. var revisedCurrentPref = getEventTimerPreferences('sequence', defaultSequence); if (revisedCurrentPref.length === 2) { setEventTimerPreferences('sequence', defaultSequence); } // Now reload otherwise people whine about category titles. mainEventTimer(true); }); // fixme - no idea why, but this line is required to make everything work. startHourUTC = now.getUTCHours(); } // Utility function #11: Generate a full day of meta pattern function eventsGenerator(eventData, metaSequence) { function fullPatternGenerator(partial, pattern) { // 23:00 plus 2 hour lookahead plus 15 mins future var fillDuration = 60 * 25 + 15; // Figure out total length of partial var partialDuration = 0; $.map(partial, function (v) { partialDuration += v.d; }); // If already sufficiently long, then we don't need to add any pattern sections var fullPattern; if (partialDuration >= fillDuration) { fullPattern = partial; } else { // Figure out total length of pattern var patternDuration = 0; $.map(pattern, function (v) { patternDuration += v.d; }); // Minimum number of pattern repetitions required var patternQty = Math.ceil((fillDuration - partialDuration) / patternDuration); // Repeat pattern - can use this when we remove IE support later: // var repeatedPattern = Array(patternQty).fill().map(function(){ return pattern; }); var repeatedPattern = 'z'.repeat(patternQty).split('').map(function () { return pattern; }); // Collapse nested arrays and concatenate with the initial partial pattern fullPattern = partial.concat($.map(repeatedPattern, function (v) { return v; })); } // Now insert start and end markers var sCumulative = 0; fullPattern = $.map(fullPattern, function (v) { // Don't bother appending if cumulative start time is outside range of interest if (sCumulative >= fillDuration) { return } // Update current object v.s = sCumulative; v.e = v.s + v.d; // Update for next sCumulative = v.s + v.d; // Return current - note if you try to return v then it caches the result and every object returned is the same as the last one return { r: v.r, d: v.d, s: v.s, e: v.e }; }); return fullPattern; } var fullMetas = {}; $.each(eventData, function (k, v) { // Don't bother calculating if the meta hasn't been requested if (metaSequence.indexOf(k) == -1) { return } fullMetas[k] = eventData[k]; fullMetas[k].sequences.full = fullPatternGenerator(v.sequences.partial, v.sequences.pattern); }); return fullMetas; } // Utility function #12: Move the pointer to a new horizontal location based on the current time. function movePointer(useEvenHourStart, metaSequence) { var now = new Date(); var hour = now.getUTCHours(); var minute = now.getUTCMinutes(); // Distance in percent of the 135 minute window (2 hour + 15 mins) var currentStartHourUTC = hour; var percentOfTwoHours = ((minute / 60) * 50) * (120 / 135); if (useEvenHourStart === true) { currentStartHourUTC = Math.floor(hour / 2) * 2; percentOfTwoHours = (((hour % 2) + (minute / 60)) * 50) * (120 / 135); } // Move the pointer $('.event-pointer').css('left', percentOfTwoHours + '%'); // Check if pointer has gone beyond the 1 or 2 hour mark, it will have slid to the left, in which case we need to redraw everything else too. if (startHourUTC != hour) { // Erase existing event bars $('#event-container').html(''); // Add new ones based on the new time createEventBars(useEvenHourStart, metaSequence); } // Update local time too var timezoneOffsetString = ''; if (timezoneOffset === 0) { if (twelveHourTimes == false) { $('.event-pointer span').text(pad(hour) + ':' + pad(minute) + ' UTC'); } else { $('.event-pointer span').text((((hour + 11) % 12) + 1) + ':' + pad(minute) + ' ' + (hour >= 12 ? 'PM' : 'AM')); } } else { // For positive timezones, add a plus sign before the hour offset. Negative timezones already have a minus sign. timezoneOffsetString = 'UTC' + (timezoneOffset < 0 ? timezoneOffset / 60 : '+' + timezoneOffset / 60); if (twelveHourTimes == false) { $('.event-pointer span').text(pad(now.getHours()) + ':' + pad(now.getMinutes()) + ' ' + timezoneOffsetString); } else { $('.event-pointer span').text((((now.getHours() + 11) % 12) + 1) + ':' + pad(now.getMinutes()) + ' ' + (now.getHours() >= 12 ? 'PM' : 'AM')); } } // Check if pointer is beyond 78% (avoid clashing between red and gray markers) if (percentOfTwoHours > 78) { $('.event-pointer-time').css('right', '0px'); } else { $('.event-pointer-time').css('right', 'inherit'); } } // Utility function #13: Allowing shuffling forwards and backwards function timeshiftOnClick() { // Allow user to shuffle forwards and backwards by clicking on the gray markers $('.event-limit-text').css('cursor', 'pointer'); $('.event-limit-text.next').prop('title', uitext.timeshiftnexthoverpause); $('.event-limit-text').click(function (e) { $('.event-pointer').css('left', '0%'); $('.event-pointer-time').text(uitext.timeshiftresume); $('.event-limit-text.prev').css('display', 'inherit'); $('.event-limit-text.prev').prop('title', uitext.timeshiftprevhover); $('.event-limit-text.next').prop('title', uitext.timeshiftnexthover); // Figure out if next or prev was clicked if (e.target.classList.contains('next')) { otherHourOffset = otherHourOffset + 2; } else { otherHourOffset = otherHourOffset + 22; } // Restrict it to +23 hours otherHourOffset = otherHourOffset % 24; // Check if its gone beyond midnight if (otherHourOffset + startHourUTC >= 24) { otherHourOffset = otherHourOffset - 24; } // Check if offset is back to zero if (otherHourOffset == 0) { mainEventTimer(true); } else { mainEventTimer(true, true); } }); // Restart live updating after clicking on the red marker $('.event-pointer-time').click(function () { mainEventTimer(true); }); } // Utility function #14: Refit compact timer to window width on resize. Only visible with the "Compact view" checkbox ticked. H4 headings 220px to left function fitTimerToWindowWidth() { var w = $('#mw-content-text')[0].offsetWidth; $('#EventTimerCSS3').text('#event-wrapper.compact { width: ' + (w - (220 + 20)) + 'px } '); }; // Utility function #15: Create wiki like links; inactive when on the same page as linked to. var pageTitlePattern = /(?:(?:\/wiki\/)(.*?)(?:\?|#|$)|(?:title=)(.*?)(?:&|#|$))/; function wikiLink(pageName, text) { text = text || pageName.replace(/_/g, " "); pageName = pageName.replace(/ /g, "_"); var match = pageTitlePattern.exec(location.href); var current = match[1] || match[2]; if (current === pageName) { return $(document.createElement("a")).attr("class", "mw-selflink selflink").text(text); } else { return $(document.createElement("a")).attr("href", "/wiki/" + pageName).attr("title", pageName.replace(/_/g, " ")).text(text); } } // MAIN FUNCTION function mainEventTimer(reloaded, paused) { // Collect parameter options if specified var zoneParameter = '<!--{$zone|default:""|escape:"javascript"}-->'; var excludeParameter = '<!--{$exclude|default:""|escape:"javascript"}-->'; // If the timer was reloaded via apply, or scrolled, reset event content and timers, otherwise its the first run and we need to create the preferences user interface. if (reloaded || paused) { $('#event-container').html(''); $('#event-wrapper').removeClass(); clearInterval(setIntervalHandle); } else if (zoneParameter == '') { // Display checkboxes if showing every timer (probably on the Event timers page) eventTimerPreferences(); } // Collect preferences from localStorage var useTwelveHour = getEventTimerPreferences('twelvehour', uitext.checkboxes.twelvehour.defaultvalue); var useTopTimes = getEventTimerPreferences('toptimes', uitext.checkboxes.toptimes.defaultvalue); var useCompact = getEventTimerPreferences('compact', uitext.checkboxes.compact.defaultvalue); var hideCategories = getEventTimerPreferences('hidecategories', uitext.checkboxes.hidecategories.defaultvalue); var hideHeadings = getEventTimerPreferences('hideheadings', uitext.checkboxes.hideheadings.defaultvalue); var hideChatLinks = getEventTimerPreferences('hidechatlinks', uitext.checkboxes.hidechatlinks.defaultvalue); var useEvenHourStart = getEventTimerPreferences('even', uitext.checkboxes.even.defaultvalue); // Check for sequence preferences set by a previous version of the event timer, if so, overwrite var lastVersion = getEventTimerPreferences('version', '0'); if (lastVersion != version) { setEventTimerPreferences('version', version); setEventTimerPreferences('sequence', defaultSequence); } // Respect preferences if given and the zone parameter is specified var metaSequence = getEventTimerPreferences('sequence', defaultSequence); if (zoneParameter !== '') { // Zone parameter is set // Validate zone inputs exist in the full list var whitelist = Object.keys(eventData); var zones = []; $.each(zoneParameter.replace(', ', ',').split(','), function (i, v) { if (whitelist.indexOf(v) !== -1) { zones.push(v); } }); // Check if there are no valid options remaining if (zones.length == 0) { console.log('Error - No valid options provided within the zone parameter (' + zoneParameter + ')'); return; } // Check successful, continue - overwrite metaSequence $('#event-wrapper').addClass('zone'); hideCategories = true; useCompact = false; hideHeadings = false; useTopTimes = false; hideChatLinks = false; metaSequence = []; $.each(zones, function (i, v) { metaSequence.push(v); }); } else { // Zone parameter is blank // Exclusions $.each(excludeParameter.replace(', ', ',').split(','), function (i, v) { var index = metaSequence.indexOf(v); if (index !== -1) { metaSequence.splice(index, 1); } }); // Check if there are no valid options remaining if (metaSequence.length == 0) { console.log('Error - Exclusions resulted in no valid options being provided (' + excludeParameter + ')'); return; } } // Use viewer preferences immediately where possible if (hideCategories === true) { $('#event-wrapper').addClass('hidecategories'); } if (hideHeadings === true) { $('#event-wrapper').addClass('hideheadings'); } if (useTopTimes === true) { $('#event-wrapper').addClass('toptimes'); } if (useCompact === true) { $('#event-wrapper').addClass('compact'); } if (hideChatLinks === true) { $('#event-wrapper').addClass('hidechatlinks'); } if (useTwelveHour == true) { twelveHourTimes = true; } else { twelveHourTimes = false; } // One off tasks: Draw meta event segmented-bars, enhance them, and add a static pointer. if (paused) { createEventBars(useEvenHourStart, metaSequence, otherHourOffset); } else { $('.event-limit-text.prev').css('display', 'none'); otherHourOffset = 0; customEventData = eventsGenerator(eventData, metaSequence); createEventBars(useEvenHourStart, metaSequence); movePointer(useEvenHourStart, metaSequence); // Recurring tasks: Move the pointer every 10 seconds. Every 2 hours, redraw the segmented bars setIntervalHandle = setInterval(movePointer.bind(null, useEvenHourStart, metaSequence), 10000); // bind syntax is an IE workaround } // Paranoia - recalculate compact window width if its been reloaded if (reloaded) { fitTimerToWindowWidth(); } } // DEFER LOADING SCRIPT UNTIL JQUERY IS READY. WAIT 40MS BETWEEN ATTEMPTS. function defer(method) { if (window.jQuery) { method(); } else { setTimeout(function () { defer(method) }, 40); } } // INITIALISATION defer(function () { writeTimerCSS(); // Load the event timer after loading the jquery ui module $.ajaxSetup({ cache: true }); $.getScript('/index.php?title=Widget:Event-Timer/jquery_ui_sortable_min.js&action=raw&ctype=text/javascript', function (data, textStatus, jqxhr) { // Load the main widget from above mainEventTimer(); timeshiftOnClick(); }); }); /*</nowiki>*/