PageSourceSearch

https://www.speakupaustin.org/Scripts/budgetQuestion.js

js speakupaustin.org collected 2026-10-01 13:53:03 UTC 10,438 bytes, 315 lines download raw bytes

1var BudgetQuestion = function (config) {
2  this.config = config || {
3    poll: {},
4    $element: $(),
5    voteCallback: function (optionIdArray) {},
6    unvoteCallback: function (pollId) {}
7  };
8  this.poll = this.config.poll || {};
9  this.UI = this.poll.UI;
10  this.pollId = this.poll.id;
11  this.$element = config.$element;
12  this.init();
13};
14
15BudgetQuestion.prototype.init = function () {
16  var that = this;
17  var noBudget = that.poll.data.extendedOptions.noBudget;
18  var showResultStats = that.poll.data.showResultStats;
19
20  that.retrievePartialView(function () {
21    if (noBudget) {
22      that.$element.find(".bq-box-footer").hide();
23      that.$element.find(".bq-progress-box-inner").hide();
24    } else {
25      that.$element.find(".bq-no-budget").hide();
26    }
27
28    if (showResultStats) {
29      var url = "/Poll/GetBudgetPollOptionStats/";
30      var postData = {
31        pollId: that.pollId,
32        projectId: that.poll.data.projectId
33      };
34
35      $.post(url, postData)
36        .done(function (data) {
37          $(data).each(function (index) {
38            var pollOptionId = data[index].pollOption.id;
39            var optionPercent = Math.round(data[index].optionPercent);
40            var optionDiv = that.$element.find(
41              ".bq-option[data-poll-option-id='" + pollOptionId + "']"
42            );
43            var $optionPercentDiv = $(
44              "<div class='bq-option-percent-outer' title='" +
45                optionPercent +
46                " percent of respondents selected this option'><span class='bq-option-percent'>" +
47                optionPercent +
48                "%</span></div>"
49            );
50            if ($.fn.tooltip) {
51              $optionPercentDiv.tooltip();
52            }
53            optionDiv.prepend($optionPercentDiv);
54          });
55        })
56        .fail(function (err) {
57          console.error(
58            "Error loading budget options:",
59            err.responseText || err.statusText || err
60          );
61        });
62    }
63
64    that.assignEventListeners();
65    that.updateBudgetAndSpent();
66  });
67
68  window.TOOLS && TOOLS.requireCSSFile("/Content/PollCSS/budget-question.css");
69};
70
71BudgetQuestion.prototype.retrievePartialView = function (callback) {
72  var that = this;
73
74  $.post("/Poll/GetBudgetQuestion/", { pollId: this.pollId })
75    .done(function (data) {
76      that.$element.empty().append(data);
77      callback && callback(data);
78    })
79    .fail(function (err) {
80      console.error(
81        "Error loading budget question:",
82        err.responseText || err.statusText || err
83      );
84      that.$element.html("Error loading the budget question.");
85    });
86};
87
88BudgetQuestion.prototype.assignEventListeners = function () {
89  var that = this;
90
91  // Add keyboard accessibility to budget options (WCAG 2.1.1)
92  that.$element.find(".bq-option").each(function () {
93    var $opt = $(this);
94    if (!$opt.attr("tabindex")) $opt.attr("tabindex", "0");
95    if (!$opt.attr("role")) $opt.attr("role", "button");
96    var name = $opt.find(".bq-option-text").text().trim();
97    var cost = $opt.find(".bq-option-cost").text().trim();
98    if ($opt.hasClass("add")) {
99      $opt.attr("aria-label", "Add " + name + ", cost " + cost);
100    } else {
101      $opt.attr("aria-label", "Remove " + name + ", cost " + cost);
102    }
103  });
104
105  // Keyboard support: Enter/Space to activate budget options
106  that.$element.on("keydown", ".bq-option", function (e) {
107    if (e.key === "Enter" || e.key === " ") {
108      e.preventDefault();
109      $(this).trigger("click");
110    }
111  });
112
113  that.$element.on("click", ".bq-option.add", function (e) {
114    var pollOptionId = $(this).data("pollOptionId");
115    var budget = that.poll.data.extendedOptions.budget;
116    var noBudget = that.poll.data.extendedOptions.noBudget;
117    var clickedOptionCost = parseInt(
118      $(this).find(".bq-option-cost").text().replace(/\$|,/g, "")
119    );
120    var totalCost = 0;
121
122    if (noBudget) {
123      that.addItem(pollOptionId);
124      return;
125    }
126
127    // get sum cost of options in "Chosen" list
128    that.$element.find(".bq-option.remove").each(function (index) {
129      totalCost += parseInt(
130        $(this).find(".bq-option-cost").text().replace(/\$|,/g, "")
131      );
132    });
133
134    // add cost of clicked option to total
135    totalCost += clickedOptionCost;
136
137    if (totalCost > budget) {
138      that.$element.notify("Unable to add option. Please stay within budget", {
139        className: "error",
140        position: "bottom center",
141        clickToHide: true,
142        autoHide: false
143      });
144    } else {
145      that.addItem(pollOptionId);
146    }
147  });
148
149  that.$element.on("click", ".bq-option.remove", function (e) {
150    var pollOptionId = $(this).data("pollOptionId");
151
152    that.removeItem(pollOptionId);
153  });
154};
155
156BudgetQuestion.prototype.addItem = function (pollOptionId) {
157  var that = this;
158  var $itemToAdd = that.$element.find(
159    ".bq-option[data-poll-option-id='" + pollOptionId + "']"
160  );
161  var $chosenList = that.$element.find(".bq-box.chosen .bq-box-inner");
162  var pollOptionIdArray = [];
163
164  pollOptionIdArray.push(pollOptionId);
165
166  // include poll option ids in "Chosen" list
167  that.$element.find(".bq-option.remove").each(function (index) {
168    pollOptionIdArray.push($(this).data("pollOptionId"));
169  });
170
171  $itemToAdd.find(".bq-checkbox").css("color", "rgb(76, 175, 80)");
172  $itemToAdd
173    .find("path")
174    .attr(
175      "d",
176      "M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
177    );
178
179  $itemToAdd.fadeOut(500, function () {
180    // format list item for "Chosen" list
181    $itemToAdd.removeClass("add");
182    $itemToAdd.addClass("remove");
183    $itemToAdd.find(".bq-checkbox-input")[0].checked = true;
184    // Update aria-label for remove context
185    var name = $itemToAdd.find(".bq-option-text").text().trim();
186    var cost = $itemToAdd.find(".bq-option-cost").text().trim();
187    $itemToAdd.attr("aria-label", "Remove " + name + ", cost " + cost);
188
189    // vote after option has faded in
190    $chosenList.append(
191      $itemToAdd.hide().fadeIn(500, function () {
192        that.config.voteCallback && that.config.voteCallback(pollOptionIdArray);
193        that.updateBudgetAndSpent(name + " added");
194        that.$element.notify("Option added and saved", {
195          className: "success",
196          position: "bottom center",
197          clickToHide: true,
198          autoHide: false
199        });
200      })
201    );
202  });
203};
204
205BudgetQuestion.prototype.removeItem = function (pollOptionId) {
206  var that = this;
207  var $itemToRemove = that.$element.find(
208    ".bq-option[data-poll-option-id='" + pollOptionId + "']"
209  );
210  var $choicesList = that.$element.find(".bq-box.choices .bq-box-inner");
211  var pollOptionIdArray = [];
212
213  $itemToRemove
214    .find("path")
215    .attr(
216      "d",
217      "M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
218    );
219
220  $itemToRemove.fadeOut(500, function () {
221    // format list item for "Choices" list
222    $itemToRemove.removeClass("remove");
223    $itemToRemove.addClass("add");
224    $itemToRemove.find(".bq-checkbox").css("color", "rgba(0, 0, 0, 0.54)");
225    $itemToRemove.find(".bq-checkbox-input")[0].checked = false;
226    // Update aria-label for add context
227    var name = $itemToRemove.find(".bq-option-text").text().trim();
228    var cost = $itemToRemove.find(".bq-option-cost").text().trim();
229    $itemToRemove.attr("aria-label", "Add " + name + ", cost " + cost);
230
231    $choicesList.append(
232      $itemToRemove.hide().fadeIn(500, function () {
233        // get poll option ids in "Chosen" list
234        that.$element.find(".bq-box.chosen .bq-option").each(function (index) {
235          pollOptionIdArray.push($(this).data("pollOptionId"));
236        });
237
238        // vote if there are options in the "Chosen" list, else unvote
239        if (pollOptionIdArray.length) {
240          that.config.voteCallback &&
241            that.config.voteCallback(pollOptionIdArray);
242        } else {
243          that.config.unvoteCallback && that.config.unvoteCallback(that.pollId);
244        }
245
246        that.updateBudgetAndSpent(name + " removed");
247        that.$element.notify("Option removed and saved", {
248          className: "success",
249          position: "bottom center",
250          clickToHide: true,
251          autoHide: false
252        });
253      })
254    );
255  });
256};
257
258BudgetQuestion.prototype.updateBudgetAndSpent = function (actionDescription) {
259  var that = this;
260  var budget = that.poll.data.extendedOptions.budget;
261  var noBudget = that.poll.data.extendedOptions.noBudget;
262  var remaining = 0;
263  var spent = 0;
264  var percent = 0;
265
266  that.$element.find(".bq-box.chosen .bq-option").each(function (index) {
267    spent += parseInt(
268      $(this).find(".bq-option-cost").text().replace(/\$|,/g, "")
269    );
270  });
271
272  if (noBudget) {
273    that.$element
274      .find(".bq-no-budget .bq-spent")
275      .text("$" + spent.toLocaleString());
276    return;
277  }
278
279  remaining = budget - spent;
280  percent = Math.round((spent / budget) * 100);
281
282  that.$element.find(".bq-remaining").text("$" + remaining.toLocaleString());
283  that.$element.find(".bq-spent").text("$" + spent.toLocaleString());
284  that.$element.find(".bq-percent").text(percent);
285  that.$element.find(".bq-progress").attr("aria-valuenow", percent);
286
287  // Announce budget changes to screen readers (WCAG 4.1.2)
288  var $announcer = that.$element.find(".bq-budget-announcer");
289  if (!$announcer.length) {
290    $announcer = $(
291      "<div class='sr-only bq-budget-announcer' aria-live='polite' aria-atomic='true'></div>"
292    );
293    that.$element.append($announcer);
294  }
295  var budgetStatus = noBudget
296    ? "Total spent: $" + spent.toLocaleString()
297    : "Budget remaining: $" +
298      remaining.toLocaleString() +
299      ". Spent: $" +
300      spent.toLocaleString() +
301      " (" +
302      percent +
303      "%)";
304  $announcer.text(
305    actionDescription ? actionDescription + ". " + budgetStatus : budgetStatus
306  );
307
308  if (percent === 100) {
309    that.$element.find(".bq-progress-bar").css("transform", "translateX(0%)");
310  } else {
311    that.$element
312      .find(".bq-progress-bar")
313      .css("transform", "translateX(-" + (100 - percent) + "%)");
314  }
315};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.