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"; function optionHtml(option) { const $node = $(`${option.html}`); $node.find(".discourse-local-date").each((_index, elem) => { $(elem).applyLocalDates(); }); return new RawHtml({ html: `${$node.html()}` }); } function infoTextHtml(text) { return new RawHtml({ html: `${text}`, }); } function _fetchVoters(data) { return ajax("/polls/voters.json", { data }).catch((error) => { if (error) { popupAjaxError(error); } else { bootbox.alert(I18n.t("poll.error_while_fetching_voters")); } }); } 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)); return contents; }, click(e) { if ($(e.target).closest("a").length === 0) { this.sendWidgetAction("toggleOption", this.attrs.option); } }, keyDown(e) { if (e.keyCode === 13) { 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 } = this; if (state.loading) { return; } state.loading = true; return this.sendWidgetAction("loadMore").finally( () => (state.loading = false) ); }, }); createWidget("discourse-poll-voters", { tagName: "ul.poll-voters-list", buildKey: (attrs) => `poll-voters-${attrs.optionId}`, defaultState() { return { loaded: "new", voters: [], page: 1, }; }, fetchVoters() { const { attrs, state } = this; if (state.loaded === "loading") { return; } state.loaded = "loading"; return _fetchVoters({ post_id: attrs.postId, poll_name: attrs.pollName, option_id: attrs.optionId, page: state.page, }).then((result) => { state.loaded = "loaded"; state.page += 1; const newVoters = attrs.pollType === "number" ? result.voters : result.voters[attrs.optionId]; const existingVoters = new Set( state.voters.map((voter) => voter.username) ); newVoters.forEach((voter) => { if (!existingVoters.has(voter.username)) { existingVoters.add(voter.username); state.voters.push(voter); } }); this.scheduleRerender(); }); }, loadMore() { return this.fetchVoters(); }, html(attrs, state) { if (attrs.voters && state.loaded === "new") { state.voters = attrs.voters; } const contents = state.voters.map((user) => { return h("li", [ avatarFor("tiny", { username: user.username, template: user.avatar_template, }), " ", ]); }); if (state.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}`, defaultState() { return { loaded: false }; }, fetchVoters() { const { attrs, state } = this; return _fetchVoters({ post_id: attrs.post.id, poll_name: attrs.poll.get("name"), }).then((result) => { state.voters = result.voters; this.scheduleRerender(); }); }, html(attrs, state) { const { poll } = attrs; const options = poll.get("options"); if (options) { const voters = poll.get("voters"); const isPublic = poll.get("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; } }); if (isPublic && !state.loaded) { state.voters = poll.get("preloaded_voters"); state.loaded = true; } 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)]) ) ); 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.get("name"), totalVotes: option.votes, voters: (state.voters && state.voters[option.id]) || [], }) ); } return h("li", { className: `${chosen ? "chosen" : ""}` }, contents); }); } }, }); createWidget("discourse-poll-number-results", { buildKey: (attrs) => `poll-number-results-${attrs.id}`, defaultState() { return { loaded: false }; }, fetchVoters() { const { attrs, state } = this; return _fetchVoters({ post_id: attrs.post.id, poll_name: attrs.poll.get("name"), }).then((result) => { state.voters = result.voters; this.scheduleRerender(); }); }, html(attrs, state) { const { poll } = attrs; const totalScore = poll.get("options").reduce((total, o) => { return total + parseInt(o.html, 10) * parseInt(o.votes, 10); }, 0); const voters = poll.get("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: `${averageRating}` }) ), ]; if (poll.get("public")) { if (!state.loaded) { state.voters = poll.get("preloaded_voters"); state.loaded = true; } contents.push( this.attach("discourse-poll-voters", { totalVotes: poll.get("voters"), voters: state.voters || [], postId: attrs.post.id, pollName: poll.get("name"), pollType: poll.get("type"), }) ); } return contents; }, }); createWidget("discourse-poll-container", { tagName: "div.poll-container", html(attrs) { const { poll } = attrs; const options = poll.get("options"); if (attrs.showResults) { const contents = []; if (attrs.titleHTML) { contents.push(new RawHtml({ html: attrs.titleHTML })); } const type = poll.get("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, attrs)); 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; } }, }); 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.get("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.get("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.get("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); const el = document.getElementById(`poll-results-chart-${attrs.id}`); // eslint-disable-next-line let chart = new Chart(el.getContext("2d"), config); document.getElementById( `poll-results-legend-${attrs.id}` ).innerHTML = chart.generateLegend(); }); }, 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(`div#poll-results-legend-${attrs.id}.pie-chart-legends`)); return contents; }, }); 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, }, options: { responsive: true, aspectRatio, animation: { duration: 0 }, legend: { display: false }, legendCallback: function (chart) { let legends = ""; for (let i = 0; i < labels.length; i++) { legends += `