|
| 1 | +import $ from 'jquery'; |
| 2 | +import {POST} from '../modules/fetch.js'; |
| 3 | +import {hideElem, showElem, toggleElem} from '../utils/dom.js'; |
| 4 | +import {showErrorToast} from '../modules/toast.js'; |
| 5 | + |
| 6 | +export function initGlobalButtonClickOnEnter() { |
| 7 | + $(document).on('keypress', 'div.ui.button,span.ui.button', (e) => { |
| 8 | + if (e.code === ' ' || e.code === 'Enter') { |
| 9 | + $(e.target).trigger('click'); |
| 10 | + e.preventDefault(); |
| 11 | + } |
| 12 | + }); |
| 13 | +} |
| 14 | + |
| 15 | +export function initGlobalDeleteButton() { |
| 16 | + // ".delete-button" shows a confirmation modal defined by `data-modal-id` attribute. |
| 17 | + // Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes. |
| 18 | + // If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification). |
| 19 | + // If there is no form, then the data will be posted to `data-url`. |
| 20 | + // TODO: it's not encouraged to use this method. `show-modal` does far better than this. |
| 21 | + for (const btn of document.querySelectorAll('.delete-button')) { |
| 22 | + btn.addEventListener('click', (e) => { |
| 23 | + e.preventDefault(); |
| 24 | + |
| 25 | + // eslint-disable-next-line github/no-dataset -- code depends on the camel-casing |
| 26 | + const dataObj = btn.dataset; |
| 27 | + |
| 28 | + const modalId = btn.getAttribute('data-modal-id'); |
| 29 | + const modal = document.querySelector(`.delete.modal${modalId ? `#${modalId}` : ''}`); |
| 30 | + |
| 31 | + // set the modal "display name" by `data-name` |
| 32 | + const modalNameEl = modal.querySelector('.name'); |
| 33 | + if (modalNameEl) modalNameEl.textContent = btn.getAttribute('data-name'); |
| 34 | + |
| 35 | + // fill the modal elements with data-xxx attributes: `data-data-organization-name="..."` => `<span class="dataOrganizationName">...</span>` |
| 36 | + for (const [key, value] of Object.entries(dataObj)) { |
| 37 | + if (key.startsWith('data')) { |
| 38 | + const textEl = modal.querySelector(`.${key}`); |
| 39 | + if (textEl) textEl.textContent = value; |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + $(modal).modal({ |
| 44 | + closable: false, |
| 45 | + onApprove: async () => { |
| 46 | + // if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."` |
| 47 | + if (btn.getAttribute('data-type') === 'form') { |
| 48 | + const formSelector = btn.getAttribute('data-form'); |
| 49 | + const form = document.querySelector(formSelector); |
| 50 | + if (!form) throw new Error(`no form named ${formSelector} found`); |
| 51 | + form.submit(); |
| 52 | + } |
| 53 | + |
| 54 | + // prepare an AJAX form by data attributes |
| 55 | + const postData = new FormData(); |
| 56 | + for (const [key, value] of Object.entries(dataObj)) { |
| 57 | + if (key.startsWith('data')) { // for data-data-xxx (HTML) -> dataXxx (form) |
| 58 | + postData.append(key.slice(4), value); |
| 59 | + } |
| 60 | + if (key === 'id') { // for data-id="..." |
| 61 | + postData.append('id', value); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + const response = await POST(btn.getAttribute('data-url'), {data: postData}); |
| 66 | + if (response.ok) { |
| 67 | + const data = await response.json(); |
| 68 | + window.location.href = data.redirect; |
| 69 | + } |
| 70 | + }, |
| 71 | + }).modal('show'); |
| 72 | + }); |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +export function initGlobalButtons() { |
| 77 | + // There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form. |
| 78 | + // However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission. |
| 79 | + // There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content") |
| 80 | + $(document).on('click', 'form button.ui.cancel.button', (e) => { |
| 81 | + e.preventDefault(); |
| 82 | + }); |
| 83 | + |
| 84 | + $('.show-panel').on('click', function (e) { |
| 85 | + // a '.show-panel' element can show a panel, by `data-panel="selector"` |
| 86 | + // if it has "toggle" class, it toggles the panel |
| 87 | + e.preventDefault(); |
| 88 | + const sel = this.getAttribute('data-panel'); |
| 89 | + if (this.classList.contains('toggle')) { |
| 90 | + toggleElem(sel); |
| 91 | + } else { |
| 92 | + showElem(sel); |
| 93 | + } |
| 94 | + }); |
| 95 | + |
| 96 | + $('.hide-panel').on('click', function (e) { |
| 97 | + // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"` |
| 98 | + e.preventDefault(); |
| 99 | + let sel = this.getAttribute('data-panel'); |
| 100 | + if (sel) { |
| 101 | + hideElem($(sel)); |
| 102 | + return; |
| 103 | + } |
| 104 | + sel = this.getAttribute('data-panel-closest'); |
| 105 | + if (sel) { |
| 106 | + hideElem($(this).closest(sel)); |
| 107 | + return; |
| 108 | + } |
| 109 | + // should never happen, otherwise there is a bug in code |
| 110 | + showErrorToast('Nothing to hide'); |
| 111 | + }); |
| 112 | +} |
| 113 | + |
| 114 | +export function initGlobalShowModal() { |
| 115 | + // A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute. |
| 116 | + // Each "data-modal-{target}" attribute will be filled to target element's value or text-content. |
| 117 | + // * First, try to query '#target' |
| 118 | + // * Then, try to query '.target' |
| 119 | + // * Then, try to query 'target' as HTML tag |
| 120 | + // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set. |
| 121 | + $('.show-modal').on('click', function (e) { |
| 122 | + e.preventDefault(); |
| 123 | + const modalSelector = this.getAttribute('data-modal'); |
| 124 | + const $modal = $(modalSelector); |
| 125 | + if (!$modal.length) { |
| 126 | + throw new Error('no modal for this action'); |
| 127 | + } |
| 128 | + const modalAttrPrefix = 'data-modal-'; |
| 129 | + for (const attrib of this.attributes) { |
| 130 | + if (!attrib.name.startsWith(modalAttrPrefix)) { |
| 131 | + continue; |
| 132 | + } |
| 133 | + |
| 134 | + const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length); |
| 135 | + const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.'); |
| 136 | + // try to find target by: "#target" -> ".target" -> "target tag" |
| 137 | + let $attrTarget = $modal.find(`#${attrTargetName}`); |
| 138 | + if (!$attrTarget.length) $attrTarget = $modal.find(`.${attrTargetName}`); |
| 139 | + if (!$attrTarget.length) $attrTarget = $modal.find(`${attrTargetName}`); |
| 140 | + if (!$attrTarget.length) continue; // TODO: show errors in dev mode to remind developers that there is a bug |
| 141 | + |
| 142 | + if (attrTargetAttr) { |
| 143 | + $attrTarget[0][attrTargetAttr] = attrib.value; |
| 144 | + } else if ($attrTarget[0].matches('input, textarea')) { |
| 145 | + $attrTarget.val(attrib.value); // FIXME: add more supports like checkbox |
| 146 | + } else { |
| 147 | + $attrTarget[0].textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + $modal.modal('setting', { |
| 152 | + onApprove: () => { |
| 153 | + // "form-fetch-action" can handle network errors gracefully, |
| 154 | + // so keep the modal dialog to make users can re-submit the form if anything wrong happens. |
| 155 | + if ($modal.find('.form-fetch-action').length) return false; |
| 156 | + }, |
| 157 | + }).modal('show'); |
| 158 | + }); |
| 159 | +} |
0 commit comments