mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 10:43:43 +08:00
1082 lines
28 KiB
JavaScript
1082 lines
28 KiB
JavaScript
import I18n from "I18n";
|
|
import { PIE_CHART_TYPE } from "discourse/plugins/poll/controllers/poll-ui-builder";
|
|
import RawHtml from "discourse/widgets/raw-html";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import { avatarFor } from "discourse/widgets/post";
|
|
import { createWidget } from "discourse/widgets/widget";
|
|
import evenRound from "discourse/plugins/poll/lib/even-round";
|
|
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
|
import { h } from "virtual-dom";
|
|
import { iconNode } from "discourse-common/lib/icon-library";
|
|
import loadScript from "discourse/lib/load-script";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { relativeAge } from "discourse/lib/formatter";
|
|
import round from "discourse/lib/round";
|
|
import showModal from "discourse/lib/show-modal";
|
|
import { applyLocalDates } from "discourse/lib/local-dates";
|
|
|
|
const FETCH_VOTERS_COUNT = 25;
|
|
|
|
function optionHtml(option, siteSettings = {}) {
|
|
const el = document.createElement("span");
|
|
el.innerHTML = option.html;
|
|
applyLocalDates(el.querySelectorAll(".discourse-local-date"), siteSettings);
|
|
return new RawHtml({ html: `<span>${el.innerHTML}</span>` });
|
|
}
|
|
|
|
function infoTextHtml(text) {
|
|
return new RawHtml({
|
|
html: `<span class="info-text">${text}</span>`,
|
|
});
|
|
}
|
|
|
|
function checkUserGroups(user, poll) {
|
|
const pollGroups =
|
|
poll && poll.groups && poll.groups.split(",").map((g) => g.toLowerCase());
|
|
|
|
if (!pollGroups) {
|
|
return true;
|
|
}
|
|
|
|
const userGroups =
|
|
user && user.groups && user.groups.map((g) => g.name.toLowerCase());
|
|
|
|
return userGroups && pollGroups.some((g) => userGroups.includes(g));
|
|
}
|
|
|
|
createWidget("discourse-poll-option", {
|
|
tagName: "li",
|
|
|
|
buildAttributes(attrs) {
|
|
return { tabindex: 0, "data-poll-option-id": attrs.option.id };
|
|
},
|
|
|
|
html(attrs) {
|
|
const contents = [];
|
|
const { option, vote } = attrs;
|
|
const chosen = vote.includes(option.id);
|
|
|
|
if (attrs.isMultiple) {
|
|
contents.push(iconNode(chosen ? "far-check-square" : "far-square"));
|
|
} else {
|
|
contents.push(iconNode(chosen ? "circle" : "far-circle"));
|
|
}
|
|
|
|
contents.push(" ");
|
|
contents.push(optionHtml(option, this.siteSettings));
|
|
|
|
return contents;
|
|
},
|
|
|
|
click(e) {
|
|
if (!e.target.closest("a")) {
|
|
this.sendWidgetAction("toggleOption", this.attrs.option);
|
|
}
|
|
},
|
|
|
|
keyDown(e) {
|
|
if (e.key === "Enter") {
|
|
this.click(e);
|
|
}
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-load-more", {
|
|
tagName: "div.poll-voters-toggle-expand",
|
|
buildKey: (attrs) => `load-more-${attrs.optionId}`,
|
|
|
|
defaultState() {
|
|
return { loading: false };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
return state.loading
|
|
? h("div.spinner.small")
|
|
: h("a", iconNode("chevron-down"));
|
|
},
|
|
|
|
click() {
|
|
const { state, attrs } = this;
|
|
|
|
if (state.loading) {
|
|
return;
|
|
}
|
|
|
|
state.loading = true;
|
|
return this.sendWidgetAction("fetchVoters", attrs.optionId).finally(
|
|
() => (state.loading = false)
|
|
);
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-voters", {
|
|
tagName: "ul.poll-voters-list",
|
|
buildKey: (attrs) => `poll-voters-${attrs.optionId}`,
|
|
|
|
html(attrs) {
|
|
const contents = attrs.voters.map((user) =>
|
|
h("li", [
|
|
avatarFor("tiny", {
|
|
username: user.username,
|
|
template: user.avatar_template,
|
|
}),
|
|
" ",
|
|
])
|
|
);
|
|
|
|
if (attrs.voters.length < attrs.totalVotes) {
|
|
contents.push(this.attach("discourse-poll-load-more", attrs));
|
|
}
|
|
|
|
return h("div.poll-voters", contents);
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-standard-results", {
|
|
tagName: "ul.results",
|
|
buildKey: (attrs) => `poll-standard-results-${attrs.id}`,
|
|
|
|
html(attrs) {
|
|
const { poll } = attrs;
|
|
const options = poll.options;
|
|
|
|
if (options) {
|
|
const voters = poll.voters;
|
|
const isPublic = poll.public;
|
|
|
|
const ordered = [...options].sort((a, b) => {
|
|
if (a.votes < b.votes) {
|
|
return 1;
|
|
} else if (a.votes === b.votes) {
|
|
if (a.html < b.html) {
|
|
return -1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
});
|
|
|
|
const percentages =
|
|
voters === 0
|
|
? Array(ordered.length).fill(0)
|
|
: ordered.map((o) => (100 * o.votes) / voters);
|
|
|
|
const rounded = attrs.isMultiple
|
|
? percentages.map(Math.floor)
|
|
: evenRound(percentages);
|
|
|
|
return ordered.map((option, idx) => {
|
|
const contents = [];
|
|
const per = rounded[idx].toString();
|
|
const chosen = (attrs.vote || []).includes(option.id);
|
|
|
|
contents.push(
|
|
h(
|
|
"div.option",
|
|
h("p", [
|
|
h("span.percentage", `${per}%`),
|
|
optionHtml(option, this.siteSettings),
|
|
])
|
|
)
|
|
);
|
|
|
|
contents.push(
|
|
h(
|
|
"div.bar-back",
|
|
h("div.bar", { attributes: { style: `width:${per}%` } })
|
|
)
|
|
);
|
|
|
|
if (isPublic) {
|
|
contents.push(
|
|
this.attach("discourse-poll-voters", {
|
|
postId: attrs.post.id,
|
|
optionId: option.id,
|
|
pollName: poll.name,
|
|
totalVotes: option.votes,
|
|
voters: (attrs.voters && attrs.voters[option.id]) || [],
|
|
})
|
|
);
|
|
}
|
|
|
|
return h("li", { className: `${chosen ? "chosen" : ""}` }, contents);
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-number-results", {
|
|
buildKey: (attrs) => `poll-number-results-${attrs.id}`,
|
|
|
|
html(attrs) {
|
|
const { poll } = attrs;
|
|
|
|
const totalScore = poll.options.reduce((total, o) => {
|
|
return total + parseInt(o.html, 10) * parseInt(o.votes, 10);
|
|
}, 0);
|
|
|
|
const voters = poll.voters;
|
|
const average = voters === 0 ? 0 : round(totalScore / voters, -2);
|
|
const averageRating = I18n.t("poll.average_rating", { average });
|
|
const contents = [
|
|
h(
|
|
"div.poll-results-number-rating",
|
|
new RawHtml({ html: `<span>${averageRating}</span>` })
|
|
),
|
|
];
|
|
|
|
if (poll.public) {
|
|
contents.push(
|
|
this.attach("discourse-poll-voters", {
|
|
totalVotes: poll.voters,
|
|
voters: attrs.voters || [],
|
|
postId: attrs.post.id,
|
|
pollName: poll.name,
|
|
pollType: poll.type,
|
|
})
|
|
);
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-container", {
|
|
tagName: "div.poll-container",
|
|
buildKey: (attrs) => `poll-container-${attrs.id}`,
|
|
services: ["dialog"],
|
|
|
|
defaultState() {
|
|
return { voters: [] };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
const { poll } = attrs;
|
|
const options = poll.options;
|
|
|
|
if (attrs.showResults) {
|
|
const contents = [];
|
|
|
|
if (attrs.titleHTML) {
|
|
contents.push(new RawHtml({ html: attrs.titleHTML }));
|
|
}
|
|
|
|
if (poll.public) {
|
|
state.voters = poll.preloaded_voters;
|
|
}
|
|
|
|
const type = poll.type === "number" ? "number" : "standard";
|
|
const resultsWidget =
|
|
type === "number" || attrs.poll.chart_type !== PIE_CHART_TYPE
|
|
? `discourse-poll-${type}-results`
|
|
: "discourse-poll-pie-chart";
|
|
contents.push(
|
|
this.attach(
|
|
resultsWidget,
|
|
Object.assign({}, attrs, { voters: state.voters })
|
|
)
|
|
);
|
|
|
|
return contents;
|
|
} else if (options) {
|
|
const contents = [];
|
|
|
|
if (attrs.titleHTML) {
|
|
contents.push(new RawHtml({ html: attrs.titleHTML }));
|
|
}
|
|
|
|
if (!checkUserGroups(this.currentUser, poll)) {
|
|
contents.push(
|
|
h(
|
|
"div.alert.alert-danger",
|
|
I18n.t("poll.results.groups.title", { groups: poll.groups })
|
|
)
|
|
);
|
|
}
|
|
|
|
contents.push(
|
|
h(
|
|
"ul",
|
|
options.map((option) => {
|
|
return this.attach("discourse-poll-option", {
|
|
option,
|
|
isMultiple: attrs.isMultiple,
|
|
vote: attrs.vote,
|
|
});
|
|
})
|
|
)
|
|
);
|
|
|
|
return contents;
|
|
}
|
|
},
|
|
|
|
fetchVoters(optionId) {
|
|
const { attrs, state } = this;
|
|
let votersCount;
|
|
|
|
if (optionId) {
|
|
if (!state.voters) {
|
|
state.voters = {};
|
|
}
|
|
|
|
if (!state.voters[optionId]) {
|
|
state.voters[optionId] = [];
|
|
}
|
|
|
|
votersCount = state.voters[optionId].length;
|
|
} else {
|
|
if (!state.voters) {
|
|
state.voters = [];
|
|
}
|
|
|
|
votersCount = state.voters.length;
|
|
}
|
|
|
|
return ajax("/polls/voters.json", {
|
|
data: {
|
|
post_id: attrs.post.id,
|
|
poll_name: attrs.poll.name,
|
|
option_id: optionId,
|
|
page: Math.floor(votersCount / FETCH_VOTERS_COUNT) + 1,
|
|
limit: FETCH_VOTERS_COUNT,
|
|
},
|
|
})
|
|
.then((result) => {
|
|
const voters = optionId ? state.voters[optionId] : state.voters;
|
|
const newVoters = optionId ? result.voters[optionId] : result.voters;
|
|
|
|
const votersSet = new Set(voters.map((voter) => voter.username));
|
|
newVoters.forEach((voter) => {
|
|
if (!votersSet.has(voter.username)) {
|
|
votersSet.add(voter.username);
|
|
voters.push(voter);
|
|
}
|
|
});
|
|
|
|
// remove users who changed their vote
|
|
if (attrs.poll.type === "regular") {
|
|
Object.keys(state.voters).forEach((otherOptionId) => {
|
|
if (optionId !== otherOptionId) {
|
|
state.voters[otherOptionId] = state.voters[otherOptionId].filter(
|
|
(voter) => !votersSet.has(voter.username)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.scheduleRerender();
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
this.dialog.alert(I18n.t("poll.error_while_fetching_voters"));
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-info", {
|
|
tagName: "div.poll-info",
|
|
|
|
multipleHelpText(min, max, options) {
|
|
if (max > 0) {
|
|
if (min === max) {
|
|
if (min > 1) {
|
|
return I18n.t("poll.multiple.help.x_options", { count: min });
|
|
}
|
|
} else if (min > 1) {
|
|
if (max < options) {
|
|
return I18n.t("poll.multiple.help.between_min_and_max_options", {
|
|
min,
|
|
max,
|
|
});
|
|
} else {
|
|
return I18n.t("poll.multiple.help.at_least_min_options", {
|
|
count: min,
|
|
});
|
|
}
|
|
} else if (max <= options) {
|
|
return I18n.t("poll.multiple.help.up_to_max_options", { count: max });
|
|
}
|
|
}
|
|
},
|
|
|
|
html(attrs) {
|
|
const { poll } = attrs;
|
|
const count = poll.voters;
|
|
const contents = [
|
|
h("p", [
|
|
h("span.info-number", count.toString()),
|
|
h("span.info-label", I18n.t("poll.voters", { count })),
|
|
]),
|
|
];
|
|
|
|
if (attrs.isMultiple) {
|
|
if (attrs.showResults || attrs.isClosed) {
|
|
const totalVotes = poll.options.reduce((total, o) => {
|
|
return total + parseInt(o.votes, 10);
|
|
}, 0);
|
|
|
|
contents.push(
|
|
h("p", [
|
|
h("span.info-number", totalVotes.toString()),
|
|
h(
|
|
"span.info-label",
|
|
I18n.t("poll.total_votes", { count: totalVotes })
|
|
),
|
|
])
|
|
);
|
|
} else {
|
|
const help = this.multipleHelpText(
|
|
attrs.min,
|
|
attrs.max,
|
|
poll.options.length
|
|
);
|
|
if (help) {
|
|
contents.push(infoTextHtml(help));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
!attrs.isClosed &&
|
|
!attrs.showResults &&
|
|
poll.public &&
|
|
poll.results !== "staff_only"
|
|
) {
|
|
contents.push(infoTextHtml(I18n.t("poll.public.title")));
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
function clearPieChart(id) {
|
|
let el = document.querySelector(`#poll-results-chart-${id}`);
|
|
el && el.parentNode.removeChild(el);
|
|
}
|
|
|
|
createWidget("discourse-poll-pie-canvas", {
|
|
tagName: "canvas.poll-results-canvas",
|
|
|
|
init(attrs) {
|
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
const data = attrs.poll.options.mapBy("votes");
|
|
const labels = attrs.poll.options.mapBy("html");
|
|
const config = pieChartConfig(data, labels, {
|
|
legendContainerId: `poll-results-legend-${attrs.id}`,
|
|
});
|
|
|
|
const el = document.getElementById(`poll-results-chart-${attrs.id}`);
|
|
// eslint-disable-next-line no-undef
|
|
this._chart = new Chart(el.getContext("2d"), config);
|
|
});
|
|
},
|
|
|
|
willRerenderWidget() {
|
|
this._chart?.destroy();
|
|
},
|
|
|
|
buildAttributes(attrs) {
|
|
return {
|
|
id: `poll-results-chart-${attrs.id}`,
|
|
};
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-pie-chart", {
|
|
tagName: "div.poll-results-chart",
|
|
|
|
html(attrs) {
|
|
const contents = [];
|
|
|
|
if (!attrs.showResults) {
|
|
clearPieChart(attrs.id);
|
|
return contents;
|
|
}
|
|
|
|
const chart = this.attach("discourse-poll-pie-canvas", attrs);
|
|
contents.push(chart);
|
|
|
|
contents.push(h(`ul#poll-results-legend-${attrs.id}.pie-chart-legends`));
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
const htmlLegendPlugin = {
|
|
id: "htmlLegend",
|
|
|
|
afterUpdate(chart, args, options) {
|
|
const ul = document.getElementById(options.containerID);
|
|
ul.innerHTML = "";
|
|
|
|
const items = chart.options.plugins.legend.labels.generateLabels(chart);
|
|
items.forEach((item) => {
|
|
const li = document.createElement("li");
|
|
li.classList.add("legend");
|
|
li.onclick = () => {
|
|
chart.toggleDataVisibility(item.index);
|
|
chart.update();
|
|
};
|
|
|
|
const boxSpan = document.createElement("span");
|
|
boxSpan.classList.add("swatch");
|
|
boxSpan.style.background = item.fillStyle;
|
|
|
|
const textContainer = document.createElement("span");
|
|
textContainer.style.color = item.fontColor;
|
|
textContainer.innerHTML = item.text;
|
|
|
|
if (!chart.getDataVisibility(item.index)) {
|
|
li.style.opacity = 0.2;
|
|
} else {
|
|
li.style.opacity = 1.0;
|
|
}
|
|
|
|
li.appendChild(boxSpan);
|
|
li.appendChild(textContainer);
|
|
|
|
ul.appendChild(li);
|
|
});
|
|
},
|
|
};
|
|
|
|
function pieChartConfig(data, labels, opts = {}) {
|
|
const aspectRatio = "aspectRatio" in opts ? opts.aspectRatio : 2.2;
|
|
const strippedLabels = labels.map((l) => stripHtml(l));
|
|
|
|
return {
|
|
type: PIE_CHART_TYPE,
|
|
data: {
|
|
datasets: [
|
|
{
|
|
data,
|
|
backgroundColor: getColors(data.length),
|
|
},
|
|
],
|
|
labels: strippedLabels,
|
|
},
|
|
plugins: [htmlLegendPlugin],
|
|
options: {
|
|
responsive: true,
|
|
aspectRatio,
|
|
animation: { duration: 0 },
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
generateLabels() {
|
|
return labels.map((text, index) => {
|
|
return {
|
|
fillStyle: getColors(data.length)[index],
|
|
text,
|
|
index,
|
|
};
|
|
});
|
|
},
|
|
},
|
|
display: false,
|
|
},
|
|
htmlLegend: {
|
|
containerID: opts?.legendContainerId,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function stripHtml(html) {
|
|
let doc = new DOMParser().parseFromString(html, "text/html");
|
|
return doc.body.textContent || "";
|
|
}
|
|
|
|
createWidget("discourse-poll-buttons", {
|
|
tagName: "div.poll-buttons",
|
|
|
|
html(attrs) {
|
|
const contents = [];
|
|
const { poll, post } = attrs;
|
|
const topicArchived = post.get("topic.archived");
|
|
const closed = attrs.isClosed;
|
|
const staffOnly = poll.results === "staff_only";
|
|
const isStaff = this.currentUser && this.currentUser.staff;
|
|
const isAdmin = this.currentUser && this.currentUser.admin;
|
|
const isMe = this.currentUser && post.user_id === this.currentUser.id;
|
|
const dataExplorerEnabled = this.siteSettings.data_explorer_enabled;
|
|
const hideResultsDisabled = !staffOnly && (closed || topicArchived);
|
|
const exportQueryID = this.siteSettings.poll_export_data_explorer_query_id;
|
|
|
|
if (attrs.isMultiple && !hideResultsDisabled) {
|
|
const castVotesDisabled = !attrs.canCastVotes;
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: `cast-votes ${
|
|
castVotesDisabled ? "btn-default" : "btn-primary"
|
|
}`,
|
|
label: "poll.cast-votes.label",
|
|
title: "poll.cast-votes.title",
|
|
disabled: castVotesDisabled,
|
|
action: "castVotes",
|
|
})
|
|
);
|
|
contents.push(" ");
|
|
}
|
|
|
|
if (attrs.showResults || hideResultsDisabled) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn-default toggle-results",
|
|
label: "poll.hide-results.label",
|
|
title: "poll.hide-results.title",
|
|
icon: "far-eye-slash",
|
|
disabled: hideResultsDisabled,
|
|
action: "toggleResults",
|
|
})
|
|
);
|
|
} else {
|
|
let showResultsButton;
|
|
let infoText;
|
|
|
|
if (poll.results === "on_vote" && !attrs.hasVoted && !isMe) {
|
|
infoText = infoTextHtml(I18n.t("poll.results.vote.title"));
|
|
} else if (poll.results === "on_close" && !closed) {
|
|
infoText = infoTextHtml(I18n.t("poll.results.closed.title"));
|
|
} else if (poll.results === "staff_only" && !isStaff) {
|
|
infoText = infoTextHtml(I18n.t("poll.results.staff.title"));
|
|
} else {
|
|
showResultsButton = this.attach("button", {
|
|
className: "btn-default toggle-results",
|
|
label: "poll.show-results.label",
|
|
title: "poll.show-results.title",
|
|
icon: "far-eye",
|
|
disabled: poll.voters === 0,
|
|
action: "toggleResults",
|
|
});
|
|
}
|
|
|
|
if (showResultsButton) {
|
|
contents.push(showResultsButton);
|
|
}
|
|
|
|
if (attrs.hasSavedVote) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn-default remove-vote",
|
|
label: "poll.remove-vote.label",
|
|
title: "poll.remove-vote.title",
|
|
icon: "trash-alt",
|
|
action: "removeVote",
|
|
})
|
|
);
|
|
}
|
|
|
|
if (infoText) {
|
|
contents.push(infoText);
|
|
}
|
|
}
|
|
|
|
if (attrs.groupableUserFields.length && poll.voters > 0) {
|
|
const button = this.attach("button", {
|
|
className: "btn-default poll-show-breakdown",
|
|
label: "poll.group-results.label",
|
|
title: "poll.group-results.title",
|
|
icon: "far-eye",
|
|
action: "showBreakdown",
|
|
});
|
|
|
|
contents.push(button);
|
|
}
|
|
|
|
if (isAdmin && dataExplorerEnabled && poll.voters > 0 && exportQueryID) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn btn-default export-results",
|
|
label: "poll.export-results.label",
|
|
title: "poll.export-results.title",
|
|
icon: "download",
|
|
disabled: poll.voters === 0,
|
|
action: "exportResults",
|
|
})
|
|
);
|
|
}
|
|
|
|
if (poll.close) {
|
|
const closeDate = moment(poll.close);
|
|
if (closeDate.isValid()) {
|
|
const title = closeDate.format("LLL");
|
|
let label;
|
|
|
|
if (attrs.isAutomaticallyClosed) {
|
|
const age = relativeAge(closeDate.toDate(), { addAgo: true });
|
|
label = I18n.t("poll.automatic_close.age", { age });
|
|
} else {
|
|
const timeLeft = moment().to(closeDate, true);
|
|
label = I18n.t("poll.automatic_close.closes_in", { timeLeft });
|
|
}
|
|
|
|
contents.push(
|
|
new RawHtml({
|
|
html: `<span class="info-text" title="${title}">${label}</span>`,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
this.currentUser &&
|
|
(this.currentUser.id === post.user_id || isStaff) &&
|
|
!topicArchived
|
|
) {
|
|
if (closed) {
|
|
if (!attrs.isAutomaticallyClosed) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn-default toggle-status",
|
|
label: "poll.open.label",
|
|
title: "poll.open.title",
|
|
icon: "unlock-alt",
|
|
action: "toggleStatus",
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "toggle-status btn-danger",
|
|
label: "poll.close.label",
|
|
title: "poll.close.title",
|
|
icon: "lock",
|
|
action: "toggleStatus",
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
export default createWidget("discourse-poll", {
|
|
tagName: "div",
|
|
buildKey: (attrs) => `poll-${attrs.id}`,
|
|
services: ["dialog"],
|
|
|
|
buildAttributes(attrs) {
|
|
let cssClasses = "poll";
|
|
if (attrs.poll.chart_type === PIE_CHART_TYPE) {
|
|
cssClasses += " pie";
|
|
}
|
|
return {
|
|
class: cssClasses,
|
|
"data-poll-name": attrs.poll.name,
|
|
"data-poll-type": attrs.poll.type,
|
|
};
|
|
},
|
|
|
|
defaultState(attrs) {
|
|
const { poll } = attrs;
|
|
const staffOnly = attrs.poll.results === "staff_only";
|
|
|
|
const showResults =
|
|
poll.results !== "on_close" && this.hasVoted() && !staffOnly;
|
|
|
|
return { loading: false, showResults };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
const staffOnly = attrs.poll.results === "staff_only";
|
|
const showResults =
|
|
state.showResults ||
|
|
(attrs.post.get("topic.archived") && !staffOnly) ||
|
|
(this.isClosed() && !staffOnly);
|
|
|
|
const newAttrs = Object.assign({}, attrs, {
|
|
canCastVotes: this.canCastVotes(),
|
|
hasVoted: this.hasVoted(),
|
|
isAutomaticallyClosed: this.isAutomaticallyClosed(),
|
|
isClosed: this.isClosed(),
|
|
isMultiple: this.isMultiple(),
|
|
max: this.max(),
|
|
min: this.min(),
|
|
showResults,
|
|
});
|
|
|
|
return h("div", [
|
|
this.attach("discourse-poll-container", newAttrs),
|
|
this.attach("discourse-poll-info", newAttrs),
|
|
this.attach("discourse-poll-buttons", newAttrs),
|
|
]);
|
|
},
|
|
|
|
min() {
|
|
let min = parseInt(this.attrs.poll.min, 10);
|
|
if (isNaN(min) || min < 0) {
|
|
min = 1;
|
|
}
|
|
|
|
return min;
|
|
},
|
|
|
|
max() {
|
|
let max = parseInt(this.attrs.poll.max, 10);
|
|
const numOptions = this.attrs.poll.options.length;
|
|
if (isNaN(max) || max > numOptions) {
|
|
max = numOptions;
|
|
}
|
|
return max;
|
|
},
|
|
|
|
isAutomaticallyClosed() {
|
|
const { poll } = this.attrs;
|
|
return poll.close && moment.utc(poll.close) <= moment();
|
|
},
|
|
|
|
isClosed() {
|
|
const { poll } = this.attrs;
|
|
return poll.status === "closed" || this.isAutomaticallyClosed();
|
|
},
|
|
|
|
isMultiple() {
|
|
const { poll } = this.attrs;
|
|
return poll.type === "multiple";
|
|
},
|
|
|
|
hasVoted() {
|
|
const { vote } = this.attrs;
|
|
return vote && vote.length > 0;
|
|
},
|
|
|
|
canCastVotes() {
|
|
const { state, attrs } = this;
|
|
|
|
if (this.isClosed() || state.showResults || state.loading) {
|
|
return false;
|
|
}
|
|
|
|
const selectedOptionCount = attrs.vote.length;
|
|
|
|
if (this.isMultiple()) {
|
|
return (
|
|
selectedOptionCount >= this.min() && selectedOptionCount <= this.max()
|
|
);
|
|
}
|
|
|
|
return selectedOptionCount > 0;
|
|
},
|
|
|
|
toggleStatus() {
|
|
const { state, attrs } = this;
|
|
const { post, poll } = attrs;
|
|
|
|
if (this.isAutomaticallyClosed()) {
|
|
return;
|
|
}
|
|
|
|
this.dialog.yesNoConfirm({
|
|
message: I18n.t(
|
|
this.isClosed() ? "poll.open.confirm" : "poll.close.confirm"
|
|
),
|
|
didConfirm: () => {
|
|
state.loading = true;
|
|
const status = this.isClosed() ? "open" : "closed";
|
|
ajax("/polls/toggle_status", {
|
|
type: "PUT",
|
|
data: {
|
|
post_id: post.id,
|
|
poll_name: poll.name,
|
|
status,
|
|
},
|
|
})
|
|
.then(() => {
|
|
poll.set("status", status);
|
|
if (poll.results === "on_close") {
|
|
state.showResults = status === "closed";
|
|
}
|
|
this.scheduleRerender();
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
this.dialog.alert(I18n.t("poll.error_while_toggling_status"));
|
|
}
|
|
})
|
|
.finally(() => {
|
|
state.loading = false;
|
|
});
|
|
},
|
|
});
|
|
},
|
|
|
|
toggleResults() {
|
|
this.state.showResults = !this.state.showResults;
|
|
},
|
|
|
|
removeVote() {
|
|
const { attrs, state } = this;
|
|
state.loading = true;
|
|
return ajax("/polls/vote", {
|
|
type: "DELETE",
|
|
data: {
|
|
post_id: attrs.post.id,
|
|
poll_name: attrs.poll.name,
|
|
},
|
|
})
|
|
.then(({ poll }) => {
|
|
attrs.poll.setProperties(poll);
|
|
attrs.vote.length = 0;
|
|
attrs.hasSavedVote = false;
|
|
this.appEvents.trigger("poll:voted", poll, attrs.post, attrs.vote);
|
|
})
|
|
.catch((error) => popupAjaxError(error))
|
|
.finally(() => {
|
|
state.loading = false;
|
|
});
|
|
},
|
|
|
|
exportResults() {
|
|
const { attrs } = this;
|
|
const queryID = this.siteSettings.poll_export_data_explorer_query_id;
|
|
|
|
// This uses the Data Explorer plugin export as CSV route
|
|
// There is detection to check if the plugin is enabled before showing the button
|
|
ajax(`/admin/plugins/explorer/queries/${queryID}/run.csv`, {
|
|
type: "POST",
|
|
data: {
|
|
// needed for data-explorer route compatibility
|
|
params: JSON.stringify({
|
|
poll_name: attrs.poll.name,
|
|
post_id: attrs.post.id.toString(), // needed for data-explorer route compatibility
|
|
}),
|
|
explain: false,
|
|
limit: 1000000,
|
|
download: 1,
|
|
},
|
|
})
|
|
.then((csvContent) => {
|
|
const downloadLink = document.createElement("a");
|
|
const blob = new Blob([csvContent], {
|
|
type: "text/csv;charset=utf-8;",
|
|
});
|
|
downloadLink.href = URL.createObjectURL(blob);
|
|
downloadLink.setAttribute(
|
|
"download",
|
|
`poll-export-${attrs.poll.name}-${attrs.post.id}.csv`
|
|
);
|
|
downloadLink.click();
|
|
downloadLink.remove();
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
this.dialog.alert(I18n.t("poll.error_while_exporting_results"));
|
|
}
|
|
});
|
|
},
|
|
|
|
showLogin() {
|
|
this.register.lookup("route:application").send("showLogin");
|
|
},
|
|
|
|
_toggleOption(option) {
|
|
const { vote } = this.attrs;
|
|
const chosenIdx = vote.indexOf(option.id);
|
|
if (chosenIdx !== -1) {
|
|
vote.splice(chosenIdx, 1);
|
|
} else {
|
|
vote.push(option.id);
|
|
}
|
|
},
|
|
|
|
toggleOption(option) {
|
|
const { attrs } = this;
|
|
|
|
if (this.isClosed()) {
|
|
return;
|
|
}
|
|
if (!this.currentUser) {
|
|
return this.showLogin();
|
|
}
|
|
if (!checkUserGroups(this.currentUser, this.attrs.poll)) {
|
|
return;
|
|
}
|
|
|
|
const { vote } = attrs;
|
|
if (!this.isMultiple() && vote.length === 1 && vote[0] === option.id) {
|
|
return this.removeVote();
|
|
}
|
|
|
|
if (!this.isMultiple()) {
|
|
vote.length = 0;
|
|
}
|
|
|
|
this._toggleOption(option);
|
|
if (!this.isMultiple()) {
|
|
return this.castVotes().catch(() => this._toggleOption(option));
|
|
}
|
|
},
|
|
|
|
castVotes() {
|
|
if (!this.canCastVotes()) {
|
|
return;
|
|
}
|
|
if (!this.currentUser) {
|
|
return this.showLogin();
|
|
}
|
|
|
|
const { attrs, state } = this;
|
|
|
|
state.loading = true;
|
|
|
|
return ajax("/polls/vote", {
|
|
type: "PUT",
|
|
data: {
|
|
post_id: attrs.post.id,
|
|
poll_name: attrs.poll.name,
|
|
options: attrs.vote,
|
|
},
|
|
})
|
|
.then(({ poll }) => {
|
|
attrs.hasSavedVote = true;
|
|
attrs.poll.setProperties(poll);
|
|
this.appEvents.trigger("poll:voted", poll, attrs.post, attrs.vote);
|
|
|
|
if (attrs.poll.results !== "on_close") {
|
|
state.showResults = true;
|
|
}
|
|
if (attrs.poll.results === "staff_only") {
|
|
if (this.currentUser && this.currentUser.staff) {
|
|
state.showResults = true;
|
|
} else {
|
|
state.showResults = false;
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
this.dialog.alert(I18n.t("poll.error_while_casting_votes"));
|
|
}
|
|
})
|
|
.finally(() => {
|
|
state.loading = false;
|
|
});
|
|
},
|
|
|
|
showBreakdown() {
|
|
showModal("poll-breakdown", {
|
|
model: this.attrs,
|
|
panels: [
|
|
{ id: "percentage", title: "poll.breakdown.percentage" },
|
|
{ id: "count", title: "poll.breakdown.count" },
|
|
],
|
|
});
|
|
},
|
|
});
|