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.