discourse-calendar/assets/javascripts/initializers/discourse-calendar.js.es6

588 lines
17 KiB
JavaScript

import { Promise } from "rsvp";
import { isPresent } from "@ember/utils";
import DiscourseURL from "discourse/lib/url";
import { cookAsync } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
import loadScript from "discourse/lib/load-script";
import { withPluginApi } from "discourse/lib/plugin-api";
import { ajax } from "discourse/lib/ajax";
import { showPopover, hidePopover } from "discourse/lib/d-popover";
import Category from "discourse/models/category";
// https://stackoverflow.com/a/16348977
/* eslint-disable */
// prettier-ignore
function stringToHexColor(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var hex = "#";
for (var i = 0; i < 3; i++) {
var value = (hash >> (i * 8)) & 0xff;
hex += ("00" + value.toString(16)).substr(-2);
}
return hex;
}
function loadFullCalendar() {
return loadScript(
"/plugins/discourse-calendar/javascripts/fullcalendar-with-moment-timezone.min.js"
);
}
function initializeDiscourseCalendar(api) {
let _topicController;
const outletName = Discourse.SiteSettings.calendar_categories_outlet;
const site = api.container.lookup("site:main");
const isMobileView = site && site.mobileView;
let selector = `.${outletName}-outlet`;
if (outletName === "before-topic-list-body") {
selector = `.topic-list:not(.shared-drafts) .${outletName}-outlet`;
}
api.onPageChange((url, title) => {
const $calendarContainer = $(`${selector}.category-calendar`);
if (!$calendarContainer.length) return;
$calendarContainer.hide();
const browsedCategory = Category.findBySlugPathWithID(url);
if (browsedCategory) {
const settings = Discourse.SiteSettings.calendar_categories
.split("|")
.filter(Boolean)
.map(stringSetting => {
const data = {};
stringSetting
.split(";")
.filter(Boolean)
.forEach(s => {
const parts = s.split("=");
data[parts[0]] = parts[1];
});
return data;
});
const categorySetting = settings.findBy(
"categoryId",
browsedCategory.id.toString()
);
if (categorySetting && categorySetting.postId) {
$calendarContainer.show();
const postId = categorySetting.postId;
const $spinner = $(
'<div class="calendar"><div class="spinner medium"></div></div>'
);
$calendarContainer.html($spinner);
loadFullCalendar().then(() => {
const options = [`postId=${postId}`];
const optionals = ["weekends", "tzPicker", "defaultView"];
optionals.forEach(optional => {
if (isPresent(categorySetting[optional])) {
options.push(
`${optional}=${escapeExpression(categorySetting[optional])}`
);
}
});
const rawCalendar = `[calendar ${options.join(" ")}]\n[/calendar]`;
const cookRaw = cookAsync(rawCalendar);
const loadPost = ajax(`/posts/${postId}.json`);
Promise.all([cookRaw, loadPost]).then(results => {
const cooked = results[0];
const post = results[1];
const $cooked = $(cooked.string);
$calendarContainer.html($cooked);
render($(".calendar", $cooked), post);
});
});
}
}
});
api.decorateCooked(attachCalendar, {
onlyStream: true,
id: "discourse-calendar"
});
api.cleanupStream(cleanUp);
api.registerCustomPostMessageCallback("calendar_change", topicController => {
const stream = topicController.get("model.postStream");
const post = stream.findLoadedPost(stream.get("firstPostId"));
const $op = $(".topic-post article#post_1");
const $calendar = $op.find(".calendar").first();
if (post && $calendar.length > 0) {
ajax(`/posts/${post.id}.json`).then(post =>
loadFullCalendar().then(() => render($calendar, post))
);
}
});
function render($calendar, post) {
$calendar = $calendar.empty();
const timezone = _getTimeZone($calendar, api.getCurrentUser());
const calendar = _buildCalendar($calendar, timezone);
const isStatic = $calendar.attr("data-calendar-type") === "static";
if (isStatic) {
calendar.render();
_setStaticCalendarEvents(calendar, $calendar, post);
} else {
_setDynamicCalendarEvents(calendar, post);
calendar.render();
_setDynamicCalendarOptions(calendar, $calendar);
}
_setupTimezonePicker(calendar, timezone);
}
function cleanUp() {
window.removeEventListener("scroll", hidePopover);
}
function attachCalendar($elem, helper) {
window.addEventListener("scroll", hidePopover);
const $calendar = $(".calendar", $elem);
if ($calendar.length === 0) {
return;
}
loadFullCalendar().then(() => render($calendar, helper.getModel()));
}
function _buildCalendar($calendar, timeZone) {
let $calendarTitle = document.querySelector(
".discourse-calendar-header > .discourse-calendar-title"
);
const defaultView = escapeExpression(
$calendar.attr("data-calendar-default-view") ||
(isMobileView ? "listNextYear" : "month")
);
const showAddToCalendar =
$calendar.attr("data-calendar-show-add-to-calendar") !== "false";
return new window.FullCalendar.Calendar($calendar[0], {
timeZone,
timeZoneImpl: "moment-timezone",
nextDayThreshold: "06:00:00",
displayEventEnd: true,
height: 650,
firstDay: 1,
defaultView,
views: {
listNextYear: {
type: "list",
duration: { days: 365 },
buttonText: "list",
listDayFormat: {
month: "long",
year: "numeric",
day: "numeric",
weekday: "long"
}
}
},
header: {
left: "prev,next today",
center: "title",
right: "month,basicWeek,listNextYear"
},
datesRender: info => {
if (showAddToCalendar) {
_insertAddToCalendarLinks(info);
$calendarTitle.innerText = info.view.title;
}
}
});
}
function _convertHtmlToDate(html) {
const date = html.attr("data-date");
if (!date) {
return null;
}
const time = html.attr("data-time");
const timezone = html.attr("data-timezone");
let dateTime = date;
if (time) {
dateTime = `${dateTime} ${time}`;
}
return {
weeklyRecurring: html.attr("data-recurring") === "1.weeks",
dateTime: moment.tz(dateTime, timezone || "Etc/UTC")
};
}
function _buildEventObject(from, to) {
const hasTimeSpecified = d => {
return d.hours() !== 0 || d.minutes() !== 0 || d.seconds() !== 0;
};
let event = {
start: from.dateTime.toDate(),
allDay: false
};
if (to) {
if (hasTimeSpecified(to.dateTime) || hasTimeSpecified(from.dateTime)) {
event.end = to.dateTime.toDate();
} else {
event.end = to.dateTime.add(1, "days").toDate();
event.allDay = true;
}
} else {
event.allDay = true;
if (from.weeklyRecurring) {
event.startTime = {
hours: from.dateTime.hours(),
minutes: from.dateTime.minutes(),
seconds: from.dateTime.seconds()
};
event.daysOfWeek = [from.dateTime.isoWeekday()];
}
}
return event;
}
function _setStaticCalendarEvents(calendar, $calendar, post) {
$(`<div>${post.cooked}</div>`)
.find('.calendar[data-calendar-type="static"] p')
.html()
.trim()
.split("<br>")
.forEach(line => {
const html = $.parseHTML(line);
const htmlDates = html.filter(h =>
$(h).hasClass("discourse-local-date")
);
const from = _convertHtmlToDate($(htmlDates[0]));
const to = _convertHtmlToDate($(htmlDates[1]));
let event = _buildEventObject(from, to);
event.title = html[0].textContent.trim();
calendar.addEvent(event);
});
}
function _setDynamicCalendarOptions(calendar, $calendar) {
const skipWeekends = $calendar.attr("data-weekends") === "false";
const hiddenDays = $calendar.attr("data-hidden-days");
if (skipWeekends) {
calendar.setOption("weekends", false);
}
if (hiddenDays) {
calendar.setOption(
"hiddenDays",
hiddenDays.split(",").map(d => parseInt(d))
);
}
calendar.setOption("eventClick", ({ event, jsEvent }) => {
hidePopover(jsEvent);
const { htmlContent, postNumber, postUrl } = event.extendedProps;
if (postUrl) {
DiscourseURL.routeTo(postUrl);
} else if (postNumber) {
_topicController =
_topicController || api.container.lookup("controller:topic");
_topicController.send("jumpToPost", postNumber);
} else if (isMobileView && htmlContent) {
showPopover(jsEvent, { htmlContent });
}
});
calendar.setOption("eventMouseEnter", ({ event, jsEvent }) => {
const { htmlContent } = event.extendedProps;
if (!htmlContent) return;
showPopover(jsEvent, { htmlContent });
});
calendar.setOption("eventMouseLeave", ({ jsEvent }) => {
hidePopover(jsEvent);
});
}
function _buildEvent(detail) {
const event = _buildEventObject(
detail.from
? {
dateTime: moment(detail.from),
weeklyRecurring: detail.recurring === "1.weeks"
}
: null,
detail.to
? {
dateTime: moment(detail.to),
weeklyRecurring: detail.recurring === "1.weeks"
}
: null
);
event.extendedProps = {};
if (detail.post_url) {
event.extendedProps.postUrl = detail.post_url;
} else if (detail.post_number) {
event.extendedProps.postNumber = detail.post_number;
} else {
event.classNames = ["holiday"];
}
return event;
}
function _addStandaloneEvent(calendar, post, detail) {
const event = _buildEvent(detail);
const holidayCalendarTopicId = parseInt(
Discourse.SiteSettings.holiday_calendar_topic_id,
10
);
const text = detail.message.split("\n").filter(e => e);
if (
text.length &&
post.topic_id &&
holidayCalendarTopicId !== post.topic_id
) {
event.title = text[0];
event.extendedProps.description = text.slice(1).join(" ");
} else {
event.title = detail.username;
event.backgroundColor = stringToHexColor(detail.username);
}
let popupText = detail.message.substr(0, 50);
if (detail.message.length > 50) {
popupText = popupText + "...";
}
event.extendedProps.htmlContent = popupText;
event.title = event.title.replace(/<img[^>]*>/g, "");
calendar.addEvent(event);
}
function _addGroupedEvent(calendar, post, detail) {
let peopleCount = 0;
let htmlContent = "";
let usernames = [];
let localEventNames = [];
Object.keys(detail.localEvents)
.sort()
.forEach(key => {
const localEvent = detail.localEvents[key];
htmlContent += `<b>${key}</b>: ${localEvent.usernames
.sort()
.join(", ")}<br>`;
usernames = usernames.concat(localEvent.usernames);
localEventNames.push(key);
});
const event = _buildEvent(detail);
event.classNames = ["grouped-event"];
if (usernames.length > 3) {
event.title = isMobileView
? usernames.length
: `(${usernames.length}) ` + I18n.t("discourse_calendar.holiday");
} else if (usernames.length === 1) {
event.title = usernames[0];
} else {
event.title = isMobileView
? usernames.length
: `(${usernames.length}) ` + usernames.slice(0, 3).join(", ");
}
if (localEventNames.length > 1) {
event.extendedProps.htmlContent = htmlContent;
} else {
if (usernames.length > 1) {
event.extendedProps.htmlContent = htmlContent;
} else {
event.extendedProps.htmlContent = localEventNames[0];
}
}
calendar.addEvent(event);
}
function _setDynamicCalendarEvents(calendar, post) {
const groupedEvents = [];
(post.calendar_details || []).forEach(detail => {
switch (detail.type) {
case "grouped":
groupedEvents.push(detail);
break;
case "standalone":
_addStandaloneEvent(calendar, post, detail);
break;
}
});
const formatedGroupedEvents = {};
groupedEvents.forEach(groupedEvent => {
const minDate = moment(groupedEvent.from)
.utc()
.startOf("day")
.toISOString();
const maxDate = moment(groupedEvent.to || groupedEvent.from)
.utc()
.endOf("day")
.toISOString();
const identifier = `${minDate}-${maxDate}`;
formatedGroupedEvents[identifier] = formatedGroupedEvents[identifier] || {
from: minDate,
to: maxDate || minDate,
localEvents: {}
};
formatedGroupedEvents[identifier].localEvents[
groupedEvent.name
] = formatedGroupedEvents[identifier].localEvents[groupedEvent.name] || {
usernames: []
};
formatedGroupedEvents[identifier].localEvents[
groupedEvent.name
].usernames.push.apply(
formatedGroupedEvents[identifier].localEvents[groupedEvent.name]
.usernames,
groupedEvent.usernames
);
});
Object.keys(formatedGroupedEvents).forEach(key => {
const formatedGroupedEvent = formatedGroupedEvents[key];
_addGroupedEvent(calendar, post, formatedGroupedEvent);
});
}
function _getTimeZone($calendar, currentUser) {
let defaultTimezone = $calendar.attr("data-calendar-default-timezone");
const isValidDefaultTimezone = !!moment.tz.zone(defaultTimezone);
if (!isValidDefaultTimezone) {
defaultTimezone = null;
}
return (
defaultTimezone ||
(currentUser && currentUser.timezone) ||
moment.tz.guess()
);
}
function _setupTimezonePicker(calendar, timezone) {
let $timezonePicker = $(".discourse-calendar-timezone-picker");
if ($timezonePicker.length) {
$timezonePicker.on("change", function(event) {
calendar.setOption("timeZone", event.target.value);
_insertAddToCalendarLinks(calendar);
});
moment.tz.names().forEach(timezone => {
$timezonePicker.append(new Option(timezone, timezone));
});
$timezonePicker.val(timezone);
} else {
$(".discourse-calendar-timezone-wrap").text(timezone);
}
}
function _insertAddToCalendarLinks(info) {
if (info.view.type !== "listNextYear") return;
const eventSegments = info.view.eventRenderer.segs;
const eventSegmentDefMap = _eventSegmentDefMap(info);
for (const event of eventSegments) {
_insertAddToCalendarLinkForEvent(event, eventSegmentDefMap);
}
}
function _insertAddToCalendarLinkForEvent(event, eventSegmentDefMap) {
const eventTitle = event.eventRange.def.title;
let map = eventSegmentDefMap[event.eventRange.def.defId];
let startDate = map.start;
let endDate = map.end;
endDate = endDate
? _formatDateForGoogleApi(endDate, event.eventRange.def.allDay)
: _endDateForAllDayEvent(startDate, event.eventRange.def.allDay);
startDate = _formatDateForGoogleApi(startDate, event.eventRange.def.allDay);
const link = document.createElement("a");
const title = I18n.t("discourse_calendar.add_to_calendar");
link.title = title;
link.appendChild(document.createTextNode(title));
link.href = `
http://www.google.com/calendar/event?action=TEMPLATE&text=${encodeURIComponent(
eventTitle
)}&dates=${startDate}/${endDate}&details=${encodeURIComponent(
event.eventRange.def.extendedProps.description
)}`;
link.target = "_blank";
link.classList.add("fc-list-item-add-to-calendar");
event.el.querySelector(".fc-list-item-title").appendChild(link);
}
function _formatDateForGoogleApi(date, allDay = false) {
if (!allDay) return date.toISOString().replace(/-|:|\.\d\d\d/g, "");
return moment(date)
.utc()
.format("YYYYMMDD");
}
function _endDateForAllDayEvent(startDate, allDay) {
const unit = allDay ? "days" : "hours";
return _formatDateForGoogleApi(
moment(startDate)
.add(1, unit)
.toDate(),
allDay
);
}
function _eventSegmentDefMap(info) {
let map = {};
for (let event of info.view.calendar.getEvents()) {
map[event._instance.defId] = { start: event.start, end: event.end };
}
return map;
}
}
export default {
name: "discourse-calendar",
initialize(container) {
const siteSettings = container.lookup("site-settings:main");
if (siteSettings.calendar_enabled) {
withPluginApi("0.8.22", initializeDiscourseCalendar);
}
}
};