discourse/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

392 lines
12 KiB
JavaScript
Raw Normal View History

import { setOwner } from "@ember/owner";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { downloadCalendar } from "discourse/lib/download-calendar";
import { withPluginApi } from "discourse/lib/plugin-api";
import {
addTagDecorateCallback,
addTextDecorateCallback,
} from "discourse/lib/to-markdown";
import { iconHTML, renderIcon } from "discourse-common/lib/icon-library";
import { bind } from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n";
import generateDateMarkup from "discourse/plugins/discourse-local-dates/lib/local-date-markup-generator";
import LocalDatesCreateModal from "../discourse/components/modal/local-dates-create";
import LocalDateBuilder from "../lib/local-date-builder";
// Import applyLocalDates from discourse/lib/local-dates instead
export function applyLocalDates(dates, siteSettings) {
if (!siteSettings.discourse_local_dates_enabled) {
return;
}
const currentUserTZ = moment.tz.guess();
dates.forEach((element, index, arr) => {
const opts = buildOptionsFromElement(element, siteSettings);
if (
element.attributes["data-range"]?.value === "to" &&
index !== 0 &&
arr[index - 1].attributes["data-range"]?.value === "from"
) {
const fromElement = arr[index - 1];
if (_rangeIsSameLocalDay(fromElement, element)) {
opts.sameLocalDayAsFrom = true;
}
}
const localDateBuilder = new LocalDateBuilder(opts, currentUserTZ).build();
element.innerText = "";
element.insertAdjacentHTML(
"beforeend",
`${iconHTML("earth-americas")}
<span class="relative-time">${localDateBuilder.formatted}</span>`
);
element.setAttribute("aria-label", localDateBuilder.textPreview);
const classes = ["cooked-date"];
if (localDateBuilder.pastEvent) {
classes.push("past");
}
element.classList.add(...classes);
});
}
function _rangeIsSameLocalDay(fromElement, toElement) {
if (
!fromElement.attributes["data-time"] ||
!toElement.attributes["data-time"]
) {
return false;
}
const timezone = fromElement.attributes["data-timezone"].value;
const from = moment(_getDateFromElement(fromElement)).tz(timezone);
const to = moment(_getDateFromElement(toElement)).tz(timezone);
return from.isSame(to, "day");
}
function _getDateFromElement(element) {
return `${element.attributes["data-date"].value}T${element.attributes["data-time"].value}`;
}
function buildOptionsFromElement(element, siteSettings) {
const opts = {};
const dataset = element.dataset;
if (_rangeElements(element).length === 2) {
opts.duration = _calculateDuration(element);
}
opts.time = dataset.time;
opts.date = dataset.date;
opts.recurring = dataset.recurring;
opts.timezones = (
dataset.timezones ||
siteSettings.discourse_local_dates_default_timezones ||
"Etc/UTC"
)
.split("|")
.filter(Boolean);
opts.timezone = dataset.timezone;
opts.calendar = (dataset.calendar || "on") === "on";
opts.displayedTimezone = dataset.displayedTimezone;
opts.format = dataset.format || (opts.time ? "LLL" : "LL");
opts.countdown = dataset.countdown;
return opts;
}
function buildOptionsFromMarkdownTag(element) {
const opts = {};
// siteSettings defaults as used by buildOptionsFromElement are purposefully
// ommitted to reproduce exactly what was on the original element
opts.time = element.attributes["data-time"];
opts.date = element.attributes["data-date"];
opts.recurring = element.attributes["data-recurring"];
opts.timezones = element.attributes["data-timezones"];
opts.timezone = element.attributes["data-timezone"];
opts.calendar = (element.attributes["data-calendar"] || "on") === "on";
opts.displayedTimezone = element.attributes["data-displayed-timezone"];
opts.format = element.attributes["data-format"];
opts.countdown = element.attributes["data-countdown"];
opts.range = element.attributes["data-range"];
return opts;
}
function _rangeElements(element) {
if (!element.parentElement) {
return [];
}
if (element.dataset.range) {
return _partitionedRanges(element).find((pair) => pair.includes(element));
}
return [element];
}
function _partitionedRanges(element) {
const partitions = [];
const ranges = Array.from(element.parentElement.children).filter(
(span) => span.dataset.range
);
while (ranges.length > 0) {
partitions.push(ranges.splice(0, 2));
}
return partitions;
}
2018-05-04 14:51:41 +08:00
function initializeDiscourseLocalDates(api) {
const modal = api.container.lookup("service:modal");
const siteSettings = api.container.lookup("service:site-settings");
const defaultTitle = i18n("discourse_local_dates.default_title", {
site_name: siteSettings.title,
});
api.decorateCookedElement((elem, helper) => {
const dates = elem.querySelectorAll(".discourse-local-date");
applyLocalDates(dates, siteSettings);
const topicTitle = helper?.getModel()?.topic?.title;
dates.forEach((date) => {
date.dataset.title = date.dataset.title || topicTitle || defaultTitle;
});
});
2018-05-04 14:51:41 +08:00
api.onToolbarCreate((toolbar) => {
toolbar.addButton({
title: "discourse_local_dates.title",
id: "local-dates",
group: "extras",
icon: "calendar-days",
perform: (event) =>
modal.show(LocalDatesCreateModal, {
model: { insertDate: (markup) => event.addText(markup) },
}),
shortcut: "Shift+.",
shortcutAction: (event) => {
const timezone = api.getCurrentUser().user_option.timezone;
const time = moment().format("HH:mm:ss");
const date = moment().format("YYYY-MM-DD");
event.addText(`[date=${date} time=${time} timezone="${timezone}"]`);
2018-05-04 14:51:41 +08:00
},
});
2018-05-04 14:51:41 +08:00
});
addTextDecorateCallback(function (
text,
nextElement,
_previousElement,
metadata
) {
if (
metadata.discourseLocalDateStartRangeOpts &&
nextElement?.attributes.class?.includes("discourse-local-date") &&
text === "→"
) {
return "";
}
});
addTagDecorateCallback(function () {
if (this.element.attributes.class?.includes("discourse-local-date")) {
if (this.metadata.discourseLocalDateStartRangeOpts) {
const startRangeOpts = this.metadata.discourseLocalDateStartRangeOpts;
const endRangeOpts = buildOptionsFromMarkdownTag(this.element);
const markup = generateDateMarkup(
{
date: startRangeOpts.date,
time: startRangeOpts.time,
format: startRangeOpts.format,
},
endRangeOpts,
true,
{
date: endRangeOpts.date,
time: endRangeOpts.time,
format: endRangeOpts.format,
}
);
this.prefix = markup;
this.metadata.discourseLocalDateStartRangeOpts = null;
return "";
}
if (
this.element.attributes["data-range"] === "true" ||
this.element.attributes["data-range"] === "from" ||
this.element.attributes["data-range"] === "to"
) {
this.metadata.discourseLocalDateStartRangeOpts =
buildOptionsFromMarkdownTag(this.element);
return "";
}
const opts = buildOptionsFromMarkdownTag(this.element, siteSettings);
const markup = generateDateMarkup(
{ date: opts.date, time: opts.time, format: opts.format },
opts,
false
);
this.prefix = markup;
return "";
}
});
2018-05-04 14:51:41 +08:00
}
function buildHtmlPreview(element, siteSettings) {
const opts = buildOptionsFromElement(element, siteSettings);
const localDateBuilder = new LocalDateBuilder(
opts,
moment.tz.guess()
).build();
const htmlPreviews = localDateBuilder.previews.map((preview) => {
const previewNode = document.createElement("div");
previewNode.classList.add("preview");
if (preview.current) {
previewNode.classList.add("current");
}
const timezoneNode = document.createElement("span");
timezoneNode.classList.add("timezone");
timezoneNode.innerText = preview.timezone;
previewNode.appendChild(timezoneNode);
const dateTimeNode = document.createElement("span");
dateTimeNode.classList.add("date-time");
dateTimeNode.innerHTML = preview.formatted;
previewNode.appendChild(dateTimeNode);
return previewNode;
});
const previewsNode = document.createElement("div");
previewsNode.classList.add("locale-dates-previews");
htmlPreviews.forEach((htmlPreview) => previewsNode.appendChild(htmlPreview));
const calendarNode = _downloadCalendarNode(element);
if (calendarNode) {
previewsNode.appendChild(calendarNode);
}
return previewsNode.outerHTML;
}
function calculateStartAndEndDate(startDataset, endDataset) {
let startDate, endDate;
startDate = moment.tz(
`${startDataset.date} ${startDataset.time || ""}`.trim(),
startDataset.timezone
);
if (endDataset) {
endDate = moment.tz(
`${endDataset.date} ${endDataset.time || ""}`.trim(),
endDataset.timezone
);
}
return [startDate, endDate];
}
function _downloadCalendarNode(element) {
const [startDataset, endDataset] = _rangeElements(element).map(
(dateElement) => dateElement.dataset
);
const [startDate, endDate] = calculateStartAndEndDate(
startDataset,
endDataset
);
if (startDate < moment().tz(startDataset.timezone)) {
return false;
}
const node = document.createElement("div");
node.classList.add("download-calendar");
node.innerHTML = `${renderIcon("string", "file")} ${i18n(
"download_calendar.add_to_calendar"
)}`;
node.setAttribute("data-starts-at", startDate.toISOString());
if (endDataset) {
node.setAttribute("data-ends-at", endDate.toISOString());
}
if (!startDataset.time && !endDataset) {
node.setAttribute("data-ends-at", startDate.add(24, "hours").toISOString());
}
node.setAttribute("data-title", startDataset.title);
return node;
}
function _calculateDuration(element) {
const [startDataset, endDataset] = _rangeElements(element).map(
(dateElement) => dateElement.dataset
);
const startDateTime = moment(
`${startDataset.date} ${startDataset.time || ""}`.trim()
);
const endDateTime = moment(
`${endDataset.date} ${endDataset.time || ""}`.trim()
);
const duration = endDateTime.diff(startDateTime, "minutes");
// negative duration is used when we calculate difference for end date from range
return element.dataset === startDataset ? duration : -duration;
}
class LocalDatesInit {
@service siteSettings;
@service tooltip;
constructor(owner) {
setOwner(this, owner);
window.addEventListener("click", this.showDatePopover, { passive: true });
if (this.siteSettings.discourse_local_dates_enabled) {
withPluginApi("0.8.8", initializeDiscourseLocalDates);
}
}
2018-05-04 14:51:41 +08:00
DEV: FloatKit (#23650) This PR introduces three new concepts to Discourse codebase through an addon called "FloatKit": - menu - tooltip - toast ## Tooltips ### Component Simple cases can be express with an API similar to DButton: ```hbs <DTooltip @Label={{i18n "foo.bar"}} @ICON="check" @content="Something" /> ``` More complex cases can use blocks: ```hbs <DTooltip> <:trigger> {{d-icon "check"}} <span>{{i18n "foo.bar"}}</span> </:trigger> <:content> Something </:content> </DTooltip> ``` ### Service You can manually show a tooltip using the `tooltip` service: ```javascript const tooltipInstance = await this.tooltip.show( document.querySelector(".my-span"), options ) // and later manual close or destroy it tooltipInstance.close(); tooltipInstance.destroy(); // you can also just close any open tooltip through the service this.tooltip.close(); ``` The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service: ```javascript const tooltipInstance = this.tooltip.register( document.querySelector(".my-span"), options ) // when done you can destroy the instance to remove the listeners tooltipInstance.destroy(); ``` Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args: ```javascript const tooltipInstance = await this.tooltip.show( document.querySelector(".my-span"), { component: MyComponent, data: { foo: 1 } } ) ``` ## Menus Menus are very similar to tooltips and provide the same kind of APIs: ### Component ```hbs <DMenu @ICON="plus" @Label={{i18n "foo.bar"}}> <ul> <li>Foo</li> <li>Bat</li> <li>Baz</li> </ul> </DMenu> ``` They also support blocks: ```hbs <DMenu> <:trigger> {{d-icon "plus"}} <span>{{i18n "foo.bar"}}</span> </:trigger> <:content> <ul> <li>Foo</li> <li>Bat</li> <li>Baz</li> </ul> </:content> </DMenu> ``` ### Service You can manually show a menu using the `menu` service: ```javascript const menuInstance = await this.menu.show( document.querySelector(".my-span"), options ) // and later manual close or destroy it menuInstance.close(); menuInstance.destroy(); // you can also just close any open tooltip through the service this.menu.close(); ``` The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service: ```javascript const menuInstance = this.menu.register( document.querySelector(".my-span"), options ) // when done you can destroy the instance to remove the listeners menuInstance.destroy(); ``` Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args: ```javascript const menuInstance = await this.menu.show( document.querySelector(".my-span"), { component: MyComponent, data: { foo: 1 } } ) ``` ## Toasts Interacting with toasts is made only through the `toasts` service. A default component is provided (DDefaultToast) and can be used through dedicated service methods: - this.toasts.success({ ... }); - this.toasts.warning({ ... }); - this.toasts.info({ ... }); - this.toasts.error({ ... }); - this.toasts.default({ ... }); ```javascript this.toasts.success({ data: { title: "Foo", message: "Bar", actions: [ { label: "Ok", class: "btn-primary", action: (componentArgs) => { // eslint-disable-next-line no-alert alert("Closing toast:" + componentArgs.data.title); componentArgs.close(); }, } ] }, }); ``` You can also provide your own component: ```javascript this.toasts.show(MyComponent, { autoClose: false, class: "foo", data: { baz: 1 }, }) ``` Co-authored-by: Martin Brennan <mjrbrennan@gmail.com> Co-authored-by: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com> Co-authored-by: David Taylor <david@taylorhq.com> Co-authored-by: Jarek Radosz <jradosz@gmail.com>
2023-09-26 19:39:52 +08:00
@bind
showDatePopover(event) {
if (event?.target?.classList?.contains("download-calendar")) {
const dataset = event.target.dataset;
downloadCalendar(dataset.title, [
{
startsAt: dataset.startsAt,
endsAt: dataset.endsAt,
},
]);
return this.tooltip.close("local-date");
}
if (!event?.target?.classList?.contains("discourse-local-date")) {
return;
}
return this.tooltip.show(event.target, {
identifier: "local-date",
content: htmlSafe(buildHtmlPreview(event.target, this.siteSettings)),
});
}
teardown() {
window.removeEventListener("click", this.showDatePopover);
}
}
export default {
initialize(owner) {
this.instance = new LocalDatesInit(owner);
2018-05-04 14:51:41 +08:00
},
teardown() {
this.instance.teardown();
this.instance = null;
},
2018-05-04 14:51:41 +08:00
};