add_module.js
1 year ago
checkbox_group.js
1 year ago
module.js
1 year ago
module_editor.js
1 year ago
module_list.js
1 year ago
module_picker.js
1 year ago
reorder_modules.js
1 year ago
module_editor.js
455 lines
| 1 | import {Controller} from "@hotwired/stimulus" |
| 2 | |
| 3 | export default class extends Controller { |
| 4 | static targets = ['saveButton', 'cancelButton'] |
| 5 | |
| 6 | static values = { |
| 7 | reports: Array, |
| 8 | moduleId: String, |
| 9 | moduleToSwap: String |
| 10 | } |
| 11 | |
| 12 | cachedModuleMarkup = null |
| 13 | |
| 14 | connect() { |
| 15 | // React to changes in the selected report |
| 16 | if (this.element.querySelector('#report')) { |
| 17 | this.element.querySelector('#report').addEventListener('change', (event) => this.updateReport(event)) |
| 18 | } |
| 19 | |
| 20 | // Autofocus on the correct input. Since autofocus only works on page load, this needs to be |
| 21 | // done manually when editing an existing module. |
| 22 | this.element.querySelector('[autofocus]')?.focus() |
| 23 | |
| 24 | // Cancel editing if escape is pressed |
| 25 | document.addEventListener('keydown', this.onKeydown) |
| 26 | |
| 27 | if (this.moduleIdValue) { |
| 28 | this.cacheModuleMarkup() |
| 29 | } |
| 30 | |
| 31 | this.signalEditingStarted() |
| 32 | } |
| 33 | |
| 34 | disconnect() { |
| 35 | document.removeEventListener('keydown', this.onKeydown) |
| 36 | this.signalEditingFinished() |
| 37 | } |
| 38 | |
| 39 | isEditingExistingModule() { |
| 40 | return !!this.moduleIdValue |
| 41 | } |
| 42 | |
| 43 | cancel() { |
| 44 | if(this.cancelButtonTarget) { |
| 45 | this.cancelButtonTarget.disabled = true |
| 46 | } |
| 47 | |
| 48 | if (this.isSwapping()) { |
| 49 | this.element.previousSibling.classList.remove('hidden') |
| 50 | this.element.remove() |
| 51 | return |
| 52 | } |
| 53 | |
| 54 | if (!this.isEditingExistingModule()) { |
| 55 | this.signalEditingFinished() |
| 56 | this.element.outerHTML = document.getElementById('module-picker-template').innerHTML |
| 57 | } else if (this.cachedModuleMarkup) { |
| 58 | this.signalEditingFinished() |
| 59 | this.element.outerHTML = this.cachedModuleMarkup |
| 60 | } else { |
| 61 | this.cancelEditingExistingModule() |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | changeModuleType() { |
| 66 | const templateString = document.getElementById('module-picker-template').innerHTML |
| 67 | const templateDocument = new DOMParser().parseFromString(templateString, "text/html"); |
| 68 | const modulePicker = templateDocument.body.firstElementChild |
| 69 | |
| 70 | modulePicker.classList.remove('show-intro') |
| 71 | modulePicker.classList.add('show-list') |
| 72 | modulePicker.setAttribute('data-module-picker-module-to-swap-value', this.moduleIdValue) |
| 73 | |
| 74 | this.element.classList.add('hidden') |
| 75 | this.element.insertAdjacentElement('afterend', modulePicker) |
| 76 | } |
| 77 | |
| 78 | cacheModuleMarkup() { |
| 79 | const data = { |
| 80 | ...iawpActions.get_markup_for_module, |
| 81 | id: this.moduleIdValue |
| 82 | } |
| 83 | |
| 84 | jQuery.post(ajaxurl, data, (response) => { |
| 85 | this.cachedModuleMarkup = response.data.module_html |
| 86 | }) |
| 87 | } |
| 88 | |
| 89 | cancelEditingExistingModule() { |
| 90 | const data = { |
| 91 | ...iawpActions.get_markup_for_module, |
| 92 | id: this.moduleIdValue |
| 93 | } |
| 94 | |
| 95 | jQuery.post(ajaxurl, data, (response) => { |
| 96 | this.signalEditingFinished() |
| 97 | this.element.outerHTML = response.data.module_html |
| 98 | }).fail((error) => { |
| 99 | // |
| 100 | }) |
| 101 | } |
| 102 | |
| 103 | onKeydown = (event) => { |
| 104 | // Ignore non-escape keys |
| 105 | if (event.key !== "Escape") { |
| 106 | return; |
| 107 | } |
| 108 | |
| 109 | // Cancel if the active element is in the editor |
| 110 | if (this.element.contains(document.activeElement)) { |
| 111 | this.cancel() |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | updateReport(event) { |
| 116 | const newReportId = event.target.value |
| 117 | const report = this.reportsValue.find((report) => report.id === newReportId || report.id === parseInt(newReportId)) |
| 118 | |
| 119 | if (report) { |
| 120 | this.updateGroupSelect(report.columns, this.element.querySelector('#sort_by')) |
| 121 | this.updateGroupSelect(report.aggregatable_columns, this.element.querySelector('#aggregatable_sort_by')) |
| 122 | this.updateGroupSelect(report.statistics, this.element.querySelector('#primary_metric')) |
| 123 | this.updateGroupSelect(report.statistics, this.element.querySelector('#secondary_metric'), true) |
| 124 | this.updateGroupCheckboxes(report.statistics, this.element.querySelector('#statistics')) |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | save(event) { |
| 129 | event.preventDefault() |
| 130 | |
| 131 | if (this.isEditingExistingModule()) { |
| 132 | this.updateExistingModule(event) |
| 133 | } else { |
| 134 | this.saveNewModule(event) |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | updateExistingModule(event) { |
| 139 | const data = { |
| 140 | ...iawpActions.edit_module, |
| 141 | module_id: this.moduleIdValue, |
| 142 | fields: this.getFieldValues(event.target), |
| 143 | } |
| 144 | |
| 145 | this.saveButtonTarget.setAttribute('disabled', 'disabled') |
| 146 | this.saveButtonTarget.classList.add('sending') |
| 147 | |
| 148 | jQuery.post(ajaxurl, data, (response) => { |
| 149 | this.saveButtonTarget.classList.remove('sending') |
| 150 | this.saveButtonTarget.classList.add('sent') |
| 151 | setTimeout((controller) => { |
| 152 | controller.saveButtonTarget.removeAttribute('disabled') |
| 153 | controller.saveButtonTarget.classList.remove('sent') |
| 154 | controller.element.outerHTML = response.data.module_html |
| 155 | }, 500, this) |
| 156 | }).fail((error) => { |
| 157 | this.saveButtonTarget.removeAttribute('disabled') |
| 158 | this.saveButtonTarget.classList.remove('sending') |
| 159 | }) |
| 160 | } |
| 161 | |
| 162 | saveNewModule(event) { |
| 163 | const data = { |
| 164 | ...iawpActions.save_module, |
| 165 | module: this.getFieldValues(event.target), |
| 166 | moduleToSwap: this.moduleToSwapValue, |
| 167 | } |
| 168 | |
| 169 | this.saveButtonTarget.setAttribute('disabled', 'disabled') |
| 170 | this.saveButtonTarget.classList.add('sending') |
| 171 | |
| 172 | jQuery.post(ajaxurl, data, (response) => { |
| 173 | this.saveButtonTarget.classList.remove('sending') |
| 174 | this.saveButtonTarget.classList.add('sent') |
| 175 | setTimeout(() => { |
| 176 | this.saveButtonTarget.removeAttribute('disabled') |
| 177 | this.saveButtonTarget.classList.remove('sent') |
| 178 | |
| 179 | if(this.isSwapping()) { |
| 180 | this.element.previousSibling.remove() |
| 181 | this.element.insertAdjacentHTML("beforebegin", response.data.module_html) |
| 182 | this.element.remove() |
| 183 | } else { |
| 184 | this.element.insertAdjacentHTML("beforebegin", response.data.module_html) |
| 185 | this.cancel() |
| 186 | } |
| 187 | |
| 188 | // this.element.insertAdjacentHTML("beforebegin", response.data.module_html) |
| 189 | }, 500, this) |
| 190 | }).fail((error) => { |
| 191 | this.saveButtonTarget.removeAttribute('disabled') |
| 192 | this.saveButtonTarget.classList.remove('sending') |
| 193 | }) |
| 194 | } |
| 195 | |
| 196 | getFieldValues(formElement) { |
| 197 | const fields = {} |
| 198 | const formElements = Array.from(formElement.elements) |
| 199 | |
| 200 | formElements.forEach((element) => { |
| 201 | if (element.tagName === 'BUTTON' || (element.tagName === 'INPUT' && ['button', 'submit'].includes(element.type))) { |
| 202 | // Skip buttons |
| 203 | } else if (element.type === 'radio') { |
| 204 | // Special case for radio buttons |
| 205 | if (element.checked) { |
| 206 | fields[element.name] = element.value |
| 207 | } |
| 208 | } else if (element.type === 'checkbox') { |
| 209 | // Special case for checkboxes |
| 210 | if (!fields[element.name]) { |
| 211 | fields[element.name] = [] |
| 212 | } |
| 213 | if (element.checked) { |
| 214 | fields[element.name].push(element.value) |
| 215 | } |
| 216 | } else { |
| 217 | // Everything else |
| 218 | fields[element.name] = element.value |
| 219 | } |
| 220 | }) |
| 221 | |
| 222 | return fields |
| 223 | } |
| 224 | |
| 225 | // /** |
| 226 | // * Rebuild the options for the sort by select box after the report is changed |
| 227 | // * |
| 228 | // * @param data |
| 229 | // * @param selectElement |
| 230 | // */ |
| 231 | // updateSelect(data, selectElement) { |
| 232 | // if (!selectElement) { |
| 233 | // return |
| 234 | // } |
| 235 | // |
| 236 | // const previousValue = selectElement.value |
| 237 | // |
| 238 | // selectElement.innerHTML = '' |
| 239 | // |
| 240 | // data.forEach(([id, name]) => { |
| 241 | // const option = document.createElement('option') |
| 242 | // option.setAttribute('value', id) |
| 243 | // option.innerText = name |
| 244 | // selectElement.append(option) |
| 245 | // }) |
| 246 | // |
| 247 | // if (data.some(([id, name]) => id === previousValue)) { |
| 248 | // selectElement.value = previousValue |
| 249 | // } |
| 250 | // } |
| 251 | |
| 252 | /** |
| 253 | * Rebuild the options for the sort by select box after the report is changed |
| 254 | * |
| 255 | * @param data |
| 256 | * @param selectElement |
| 257 | */ |
| 258 | updateGroupSelect(data, selectElement, addNoComparisonOption = false) { |
| 259 | if (!selectElement) { |
| 260 | return |
| 261 | } |
| 262 | |
| 263 | const previousValue = selectElement.value |
| 264 | |
| 265 | selectElement.innerHTML = '' |
| 266 | |
| 267 | if (addNoComparisonOption) { |
| 268 | const optionGroup = document.createElement('optgroup') |
| 269 | optionGroup.setAttribute('label', iawpText.noComparison) |
| 270 | |
| 271 | const option = document.createElement('option') |
| 272 | option.setAttribute('value', 'no_comparison') |
| 273 | option.innerText = iawpText.noComparison |
| 274 | optionGroup.append(option) |
| 275 | |
| 276 | selectElement.appendChild(optionGroup) |
| 277 | } |
| 278 | |
| 279 | const groups = [] |
| 280 | data.forEach(([id, name, group]) => { |
| 281 | if (!groups.includes(group)) { |
| 282 | groups.push(group) |
| 283 | } |
| 284 | }) |
| 285 | |
| 286 | groups.forEach((group) => { |
| 287 | const optionGroup = document.createElement('optgroup'); |
| 288 | optionGroup.setAttribute('label', group) |
| 289 | |
| 290 | data.filter(([id, name, groupName]) => { |
| 291 | return groupName === group |
| 292 | }).forEach(([id, name]) => { |
| 293 | const option = document.createElement('option') |
| 294 | option.setAttribute('value', id) |
| 295 | option.innerText = name |
| 296 | optionGroup.append(option) |
| 297 | }) |
| 298 | |
| 299 | if (data.some(([id, name]) => id === previousValue)) { |
| 300 | selectElement.value = previousValue |
| 301 | } |
| 302 | |
| 303 | selectElement.append(optionGroup) |
| 304 | }) |
| 305 | } |
| 306 | |
| 307 | // /** |
| 308 | // * Rebuild the statistics checkboxes after the report is changed |
| 309 | // * |
| 310 | // * @param data |
| 311 | // * @param container |
| 312 | // */ |
| 313 | // updateCheckboxes(data, container) { |
| 314 | // if (!container) { |
| 315 | // return |
| 316 | // } |
| 317 | // |
| 318 | // const previousName = container.querySelector('input').name |
| 319 | // const previousValues = Array.from(container.querySelectorAll('input:checked')).map((input) => input.value) |
| 320 | // |
| 321 | // container.innerHTML = '' |
| 322 | // |
| 323 | // data.forEach(([id, name], index) => { |
| 324 | // // Create the label element |
| 325 | // const label = document.createElement('label') |
| 326 | // |
| 327 | // // Create the checkbox element |
| 328 | // const checkbox = document.createElement('input') |
| 329 | // checkbox.type = 'checkbox' |
| 330 | // checkbox.name = previousName |
| 331 | // checkbox.value = id |
| 332 | // checkbox.checked = previousValues.includes(id) || index === 0 |
| 333 | // |
| 334 | // // Create the text node |
| 335 | // const text = document.createTextNode(' ' + name) |
| 336 | // |
| 337 | // // Append the checkbox and text to the label |
| 338 | // label.appendChild(checkbox) |
| 339 | // label.appendChild(text) |
| 340 | // |
| 341 | // container.append(label) |
| 342 | // }) |
| 343 | // } |
| 344 | |
| 345 | /** |
| 346 | * Rebuild the statistics checkboxes after the report is changed |
| 347 | * |
| 348 | * @param data |
| 349 | * @param container |
| 350 | */ |
| 351 | updateGroupCheckboxes(data, container) { |
| 352 | if (!container) { |
| 353 | return |
| 354 | } |
| 355 | |
| 356 | const previousName = container.querySelector('input').name |
| 357 | const previousValues = Array.from(container.querySelectorAll('input:checked')).map((input) => input.value) |
| 358 | |
| 359 | container.innerHTML = '' |
| 360 | |
| 361 | const checkboxGroup = document.createElement('div') |
| 362 | checkboxGroup.classList.add('checkbox-group-container') |
| 363 | checkboxGroup.dataset.controller = 'checkbox-group' |
| 364 | |
| 365 | const tabContainer = document.createElement('div') |
| 366 | tabContainer.classList.add('tab-container') |
| 367 | const addedTabs = [] |
| 368 | |
| 369 | data.forEach(([id, name, group]) => { |
| 370 | if (!addedTabs.includes(group)) { |
| 371 | const tab = document.createElement('button') |
| 372 | tab.setAttribute('type', 'button') |
| 373 | tab.classList.add('checkbox-group-tab') |
| 374 | tab.dataset.groupName = group |
| 375 | tab.dataset.checkboxGroupTarget = 'groupTab' |
| 376 | tab.dataset.action = 'checkbox-group#changeTab' |
| 377 | tab.innerText = group |
| 378 | |
| 379 | // First tab should be selected |
| 380 | if (addedTabs.length === 0) { |
| 381 | tab.classList.add('selected') |
| 382 | } |
| 383 | |
| 384 | tabContainer.appendChild(tab) |
| 385 | tabContainer.append(' ') |
| 386 | addedTabs.push(group) |
| 387 | } |
| 388 | }) |
| 389 | |
| 390 | checkboxGroup.appendChild(tabContainer) |
| 391 | |
| 392 | |
| 393 | addedTabs.forEach((group, index) => { |
| 394 | const groupData = data.filter(([id, name, groupName]) => group === groupName) |
| 395 | const groupContainer = document.createElement('div'); |
| 396 | groupContainer.dataset.checkboxGroupTarget = 'group' |
| 397 | groupContainer.dataset.groupName = group |
| 398 | groupContainer.classList.add('checkbox-group') |
| 399 | |
| 400 | if (index === 0) { |
| 401 | groupContainer.classList.add('selected') |
| 402 | } |
| 403 | |
| 404 | groupData.forEach(([id, name], index) => { |
| 405 | // Create the label element |
| 406 | const label = document.createElement('label') |
| 407 | |
| 408 | // Create the checkbox element |
| 409 | const checkbox = document.createElement('input') |
| 410 | checkbox.type = 'checkbox' |
| 411 | checkbox.name = previousName |
| 412 | checkbox.value = id |
| 413 | checkbox.checked = previousValues.includes(id) || index === 0 |
| 414 | |
| 415 | // Create the text node |
| 416 | const text = document.createTextNode(' ' + name) |
| 417 | |
| 418 | // Append the checkbox and text to the label |
| 419 | label.appendChild(checkbox) |
| 420 | label.appendChild(text) |
| 421 | |
| 422 | groupContainer.appendChild(label) |
| 423 | }) |
| 424 | |
| 425 | checkboxGroup.appendChild(groupContainer) |
| 426 | }) |
| 427 | |
| 428 | container.appendChild(checkboxGroup) |
| 429 | } |
| 430 | |
| 431 | signalEditingStarted() { |
| 432 | document.dispatchEvent( |
| 433 | new CustomEvent('iawp:moduleEditingStarted', { |
| 434 | detail: { |
| 435 | moduleId: this.moduleIdValue |
| 436 | } |
| 437 | }) |
| 438 | ) |
| 439 | } |
| 440 | |
| 441 | signalEditingFinished() { |
| 442 | document.dispatchEvent( |
| 443 | new CustomEvent('iawp:moduleEditingFinished', { |
| 444 | detail: { |
| 445 | moduleId: this.moduleIdValue |
| 446 | } |
| 447 | }) |
| 448 | ) |
| 449 | } |
| 450 | |
| 451 | |
| 452 | isSwapping() { |
| 453 | return !!this.moduleToSwapValue |
| 454 | } |
| 455 | } |