2022-01-28 22:00:11 +01:00
import $ from 'jquery' ;
2023-09-06 02:02:44 +02:00
import '../vendor/jquery.are-you-sure.js' ;
2023-07-30 00:56:45 +02:00
import { clippie } from 'clippie' ;
2022-12-23 17:03:11 +01:00
import { createDropzone } from './dropzone.js' ;
2021-10-16 19:28:04 +02:00
import { initCompColorPicker } from './comp/ColorPicker.js' ;
Show messages for users if the ROOT_URL is wrong, show JavaScript errors (#18971)
* ROOT_URL issues: some users did wrong to there app.ini config, then:
* The assets can not be loaded (AppSubUrl != "" and users try to access http://host:3000/)
*The ROOT_URL is wrong, then many URLs in Gitea are broken.
Now Gitea show enough information to users.
* JavaScript error issues, there are many users affected by JavaScript errors, some are caused by frontend bugs, some are caused by broken customized templates. If these JS errors can be found at first time, then maintainers do not need to ask about how bug occurs again and again.
* Some people like to modify the `head.tmpl`, so we separate the script part to `head_script.tmpl`, then it's much safer.
* use specialized CSS class "js-global-error", end users still have a chance to hide error messages by customized CSS styles.
2022-03-30 07:52:24 +02:00
import { showGlobalErrorMessage } from '../bootstrap.js' ;
2022-08-05 12:08:29 +02:00
import { handleGlobalEnterQuickSubmit } from './comp/QuickSubmit.js' ;
2023-01-22 05:14:43 +01:00
import { svg } from '../svg.js' ;
2023-12-15 00:26:36 +01:00
import { hideElem , showElem , toggleElem , initSubmitEventPolyfill , submitEventSubmitter } from '../utils/dom.js' ;
2023-05-22 00:06:17 +02:00
import { htmlEscape } from 'escape-goat' ;
2023-08-22 04:30:02 +02:00
import { showTemporaryTooltip } from '../modules/tippy.js' ;
2023-06-19 09:46:50 +02:00
import { confirmModal } from './comp/ConfirmModal.js' ;
2023-06-27 04:45:24 +02:00
import { showErrorToast } from '../modules/toast.js' ;
2024-03-02 09:48:14 +01:00
import { request , POST , GET } from '../modules/fetch.js' ;
2024-01-30 15:45:54 +01:00
import '../htmx.js' ;
2021-10-16 19:28:04 +02:00
2023-06-16 08:32:43 +02:00
const { appUrl , appSubUrl , csrfToken , i18n } = window . config ;
2021-10-16 19:28:04 +02:00
export function initGlobalFormDirtyLeaveConfirm ( ) {
// Warn users that try to leave a page after entering data into a form.
// Except on sign-in pages, and for forms marked as 'ignore-dirty'.
if ( $ ( '.user.signin' ) . length === 0 ) {
$ ( 'form:not(.ignore-dirty)' ) . areYouSure ( ) ;
}
}
export function initHeadNavbarContentToggle ( ) {
2023-06-09 11:10:51 +02:00
const navbar = document . getElementById ( 'navbar' ) ;
const btn = document . getElementById ( 'navbar-expand-toggle' ) ;
if ( ! navbar || ! btn ) return ;
btn . addEventListener ( 'click' , ( ) => {
const isExpanded = btn . classList . contains ( 'active' ) ;
navbar . classList . toggle ( 'navbar-menu-open' , ! isExpanded ) ;
btn . classList . toggle ( 'active' , ! isExpanded ) ;
2021-10-16 19:28:04 +02:00
} ) ;
}
export function initFootLanguageMenu ( ) {
2024-03-02 09:48:14 +01:00
async function linkLanguageAction ( ) {
2021-10-16 19:28:04 +02:00
const $this = $ ( this ) ;
2024-03-02 09:48:14 +01:00
await GET ( $this . data ( 'url' ) ) ;
window . location . reload ( ) ;
2021-10-16 19:28:04 +02:00
}
$ ( '.language-menu a[lang]' ) . on ( 'click' , linkLanguageAction ) ;
}
export function initGlobalEnterQuickSubmit ( ) {
2022-05-20 04:26:04 +02:00
$ ( document ) . on ( 'keydown' , '.js-quick-submit' , ( e ) => {
if ( ( ( e . ctrlKey && ! e . altKey ) || e . metaKey ) && ( e . key === 'Enter' ) ) {
handleGlobalEnterQuickSubmit ( e . target ) ;
return false ;
2021-10-16 19:28:04 +02:00
}
} ) ;
}
export function initGlobalButtonClickOnEnter ( ) {
2023-05-21 22:47:41 +02:00
$ ( document ) . on ( 'keypress' , 'div.ui.button,span.ui.button' , ( e ) => {
if ( e . code === ' ' || e . code === 'Enter' ) {
2021-10-16 19:28:04 +02:00
$ ( e . target ) . trigger ( 'click' ) ;
2023-02-09 18:11:16 +01:00
e . preventDefault ( ) ;
2021-10-16 19:28:04 +02:00
}
} ) ;
}
2023-08-22 04:30:02 +02:00
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
2023-06-16 08:32:43 +02:00
// more details are in the backend's fetch-redirect handler
2023-08-22 04:30:02 +02:00
function fetchActionDoRedirect ( redirect ) {
2023-06-16 08:32:43 +02:00
const form = document . createElement ( 'form' ) ;
const input = document . createElement ( 'input' ) ;
form . method = 'post' ;
form . action = ` ${ appSubUrl } /-/fetch-redirect ` ;
input . type = 'hidden' ;
input . name = 'redirect' ;
input . value = redirect ;
form . append ( input ) ;
document . body . append ( form ) ;
form . submit ( ) ;
}
2023-08-22 04:30:02 +02:00
async function fetchActionDoRequest ( actionElem , url , opt ) {
try {
2023-09-19 02:50:30 +02:00
const resp = await request ( url , opt ) ;
2023-08-22 04:30:02 +02:00
if ( resp . status === 200 ) {
let { redirect } = await resp . json ( ) ;
redirect = redirect || actionElem . getAttribute ( 'data-redirect' ) ;
actionElem . classList . remove ( 'dirty' ) ; // remove the areYouSure check before reloading
if ( redirect ) {
fetchActionDoRedirect ( redirect ) ;
} else {
window . location . reload ( ) ;
}
2024-03-02 16:05:07 +01:00
return ;
2023-08-22 04:30:02 +02:00
} else if ( resp . status >= 400 && resp . status < 500 ) {
const data = await resp . json ( ) ;
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
2024-03-02 16:05:07 +01:00
if ( data . errorMessage ) {
showErrorToast ( data . errorMessage , { useHtmlBody : data . renderFormat === 'html' } ) ;
} else {
showErrorToast ( ` server error: ${ resp . status } ` ) ;
}
2023-08-22 04:30:02 +02:00
} else {
2023-08-23 09:25:13 +02:00
showErrorToast ( ` server error: ${ resp . status } ` ) ;
2023-08-22 04:30:02 +02:00
}
} catch ( e ) {
2024-03-08 10:47:32 +01:00
if ( e . name !== 'AbortError' ) {
console . error ( 'error when doRequest' , e ) ;
showErrorToast ( ` ${ i18n . network _error } ${ e } ` ) ;
}
2023-08-22 04:30:02 +02:00
}
2024-03-02 16:05:07 +01:00
actionElem . classList . remove ( 'is-loading' , 'small-loading-icon' ) ;
2023-08-22 04:30:02 +02:00
}
2023-06-14 10:01:37 +02:00
async function formFetchAction ( e ) {
if ( ! e . target . classList . contains ( 'form-fetch-action' ) ) return ;
e . preventDefault ( ) ;
const formEl = e . target ;
if ( formEl . classList . contains ( 'is-loading' ) ) return ;
formEl . classList . add ( 'is-loading' ) ;
if ( formEl . clientHeight < 50 ) {
formEl . classList . add ( 'small-loading-icon' ) ;
}
const formMethod = formEl . getAttribute ( 'method' ) || 'get' ;
const formActionUrl = formEl . getAttribute ( 'action' ) ;
const formData = new FormData ( formEl ) ;
2023-12-15 00:26:36 +01:00
const formSubmitter = submitEventSubmitter ( e ) ;
const [ submitterName , submitterValue ] = [ formSubmitter ? . getAttribute ( 'name' ) , formSubmitter ? . getAttribute ( 'value' ) ] ;
2023-06-14 10:01:37 +02:00
if ( submitterName ) {
formData . append ( submitterName , submitterValue || '' ) ;
}
let reqUrl = formActionUrl ;
2023-09-19 02:50:30 +02:00
const reqOpt = { method : formMethod . toUpperCase ( ) } ;
2023-06-14 10:01:37 +02:00
if ( formMethod . toLowerCase ( ) === 'get' ) {
const params = new URLSearchParams ( ) ;
for ( const [ key , value ] of formData ) {
params . append ( key , value . toString ( ) ) ;
}
const pos = reqUrl . indexOf ( '?' ) ;
if ( pos !== - 1 ) {
reqUrl = reqUrl . slice ( 0 , pos ) ;
}
reqUrl += ` ? ${ params . toString ( ) } ` ;
} else {
reqOpt . body = formData ;
}
2023-08-22 04:30:02 +02:00
await fetchActionDoRequest ( formEl , reqUrl , reqOpt ) ;
2023-06-14 10:01:37 +02:00
}
2021-10-16 19:28:04 +02:00
export function initGlobalCommon ( ) {
// Semantic UI modules.
2022-06-03 23:38:26 +02:00
const $uiDropdowns = $ ( '.ui.dropdown' ) ;
2023-03-17 04:08:05 +01:00
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
Fine tune more downdrop settings, use SVG for labels, improve Repo Topic Edit form (#23626)
Although it seems that some different purposes are mixed in this PR,
however, they are all related, and can be tested together, so I put them
together to save everyone's time.
Diff: `+79 −84`, everything becomes much better.
### Improve the dropdown settings.
Move all fomantic-init related code into our `fomantic.js`
Fine-tune some dropdown global settings, see the comments.
Also help to fix the first problem in #23625 , cc: @yp05327
The "language" menu has been simplified, and it works with small-height
window better.
### Use SVG instead of `<i class="delete icon">`
It's also done by `$.fn.dropdown.settings.templates.label` , cc:
@silverwind
### Remove incorrect `tabable` CSS class
It doesn't have CSS styles, and it was only in Vue. So it's totally
unnecessary, remove it by the way.
### Improve the Repo Topic Edit form
* Simplify the code
* Add a "Cancel" button
* Align elements
Before:
<details>
![image](https://user-images.githubusercontent.com/2114189/223325782-f09532de-0c38-4742-ba86-ed35cc9a858d.png)
</details>
After:
![image](https://user-images.githubusercontent.com/2114189/226796347-207feb0a-b3cd-4820-8a3e-01930bab1069.png)
2023-03-26 13:31:26 +02:00
$uiDropdowns . filter ( ':not(.custom)' ) . dropdown ( ) ;
2023-03-17 04:08:05 +01:00
// The "jump" means this dropdown is mainly used for "menu" purpose,
// clicking an item will jump to somewhere else or trigger an action/function.
// When a dropdown is used for non-refresh actions with tippy,
// it must have this "jump" class to hide the tippy when dropdown is closed.
2022-06-03 23:38:26 +02:00
$uiDropdowns . filter ( '.jump' ) . dropdown ( {
2021-10-16 19:28:04 +02:00
action : 'hide' ,
onShow ( ) {
2022-08-09 14:37:34 +02:00
// hide associated tooltip while dropdown is open
this . _tippy ? . hide ( ) ;
this . _tippy ? . disable ( ) ;
} ,
onHide ( ) {
this . _tippy ? . enable ( ) ;
2023-03-17 04:08:05 +01:00
// hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
setTimeout ( ( ) => {
const $dropdown = $ ( this ) ;
if ( $dropdown . dropdown ( 'is hidden' ) ) {
$ ( this ) . find ( '.menu > .item' ) . each ( ( _ , item ) => {
item . _tippy ? . hide ( ) ;
} ) ;
}
} , 2000 ) ;
2021-10-16 19:28:04 +02:00
} ,
} ) ;
2023-03-17 04:08:05 +01:00
2023-03-30 19:53:51 +02:00
// Special popup-directions, prevent Fomantic from guessing the popup direction.
// With default "direction: auto", if the viewport height is small, Fomantic would show the popup upward,
// if the dropdown is at the beginning of the page, then the top part would be clipped by the window view.
// eg: Issue List "Sort" dropdown
// But we can not set "direction: downward" for all dropdowns, because there is a bug in dropdown menu positioning when calculating the "left" position,
// which would make some dropdown popups slightly shift out of the right viewport edge in some cases.
// eg: the "Create New Repo" menu on the navbar.
Fine tune more downdrop settings, use SVG for labels, improve Repo Topic Edit form (#23626)
Although it seems that some different purposes are mixed in this PR,
however, they are all related, and can be tested together, so I put them
together to save everyone's time.
Diff: `+79 −84`, everything becomes much better.
### Improve the dropdown settings.
Move all fomantic-init related code into our `fomantic.js`
Fine-tune some dropdown global settings, see the comments.
Also help to fix the first problem in #23625 , cc: @yp05327
The "language" menu has been simplified, and it works with small-height
window better.
### Use SVG instead of `<i class="delete icon">`
It's also done by `$.fn.dropdown.settings.templates.label` , cc:
@silverwind
### Remove incorrect `tabable` CSS class
It doesn't have CSS styles, and it was only in Vue. So it's totally
unnecessary, remove it by the way.
### Improve the Repo Topic Edit form
* Simplify the code
* Add a "Cancel" button
* Align elements
Before:
<details>
![image](https://user-images.githubusercontent.com/2114189/223325782-f09532de-0c38-4742-ba86-ed35cc9a858d.png)
</details>
After:
![image](https://user-images.githubusercontent.com/2114189/226796347-207feb0a-b3cd-4820-8a3e-01930bab1069.png)
2023-03-26 13:31:26 +02:00
$uiDropdowns . filter ( '.upward' ) . dropdown ( 'setting' , 'direction' , 'upward' ) ;
2023-03-30 19:53:51 +02:00
$uiDropdowns . filter ( '.downward' ) . dropdown ( 'setting' , 'direction' , 'downward' ) ;
2023-03-17 04:08:05 +01:00
Use a general approch to improve a11y for all checkboxes and dropdowns. (#23542)
This PR follows #22599 and #23450
The major improvements:
1. The `aria-*.js` are totally transparent now, no need to call
`attachDropdownAria` explicitly anymore.
* It hooks the `$.fn.checkbox` and `$.fn.dropdown`, then our patch
works.
* It makes all dynamically generated checkbox/dropdown work with a11y
without any change
* eg: the `conversation.find('.dropdown').dropdown();` in `repo-diff.js`
2. Since it's totally transparent now, it could be easier to modify or
remove in the future.
3. It handles all selection labels as well (by onLabelCreate), so it
supports "multiple selection dropdown" now.
* It partially completes one of my TODOs: `TODO: multiple selection is
not supported yet.`
4. The code structure is clearer, code blocks are splitted into
different functions.
* The old `attachOneDropdownAria` was splitted into separate functions.
* It makes it easier to add more fine tunes in the future, and co-work
with contributors.
6. The code logic is similar as before, only two new parts:
1. the `ariaCheckboxFn` and `ariaDropdownFn` functions
2. the `onLabelCreate` and `updateSelectionLabel` functions
In `aria-dropdown.js` I had to mix jQuery and Vanilla JS somewhat, I
think the code is still understandable, otherwise the code would be much
more complex to read.
Thanks to fsologureng for the idea about "improving the 'delete icon'
with aria attributes".
If there is anything unclear or incorrect, feel free to ask and discuss,
or propose new PRs for it.
2023-03-22 03:52:01 +01:00
$ ( '.ui.checkbox' ) . checkbox ( ) ;
2021-11-18 04:26:50 +01:00
2021-10-16 19:28:04 +02:00
$ ( '.tabular.menu .item' ) . tab ( ) ;
2023-12-15 00:26:36 +01:00
initSubmitEventPolyfill ( ) ;
2023-06-14 10:01:37 +02:00
document . addEventListener ( 'submit' , formFetchAction ) ;
2023-08-22 04:30:02 +02:00
document . addEventListener ( 'click' , linkAction ) ;
2021-10-16 19:28:04 +02:00
}
2021-11-09 10:27:25 +01:00
export function initGlobalDropzone ( ) {
2021-10-16 19:28:04 +02:00
for ( const el of document . querySelectorAll ( '.dropzone' ) ) {
2024-02-25 07:00:55 +01:00
initDropzone ( el ) ;
}
}
export function initDropzone ( el ) {
const $dropzone = $ ( el ) ;
const _promise = createDropzone ( el , {
url : $dropzone . data ( 'upload-url' ) ,
headers : { 'X-Csrf-Token' : csrfToken } ,
maxFiles : $dropzone . data ( 'max-file' ) ,
maxFilesize : $dropzone . data ( 'max-size' ) ,
acceptedFiles : ( [ '*/*' , '' ] . includes ( $dropzone . data ( 'accepts' ) ) ) ? null : $dropzone . data ( 'accepts' ) ,
addRemoveLinks : true ,
dictDefaultMessage : $dropzone . data ( 'default-message' ) ,
dictInvalidFileType : $dropzone . data ( 'invalid-input-type' ) ,
dictFileTooBig : $dropzone . data ( 'file-too-big' ) ,
dictRemoveFile : $dropzone . data ( 'remove-file' ) ,
timeout : 0 ,
thumbnailMethod : 'contain' ,
thumbnailWidth : 480 ,
thumbnailHeight : 480 ,
init ( ) {
this . on ( 'success' , ( file , data ) => {
file . uuid = data . uuid ;
const input = $ ( ` <input id=" ${ data . uuid } " name="files" type="hidden"> ` ) . val ( data . uuid ) ;
$dropzone . find ( '.files' ) . append ( input ) ;
// Create a "Copy Link" element, to conveniently copy the image
// or file link as Markdown to the clipboard
const copyLinkElement = document . createElement ( 'div' ) ;
2024-03-04 04:33:20 +01:00
copyLinkElement . className = 'tw-text-center' ;
2024-02-25 07:00:55 +01:00
// The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
copyLinkElement . innerHTML = ` <a href="#" style="cursor: pointer;"> ${ svg ( 'octicon-copy' , 14 , 'copy link' ) } Copy link</a> ` ;
copyLinkElement . addEventListener ( 'click' , async ( e ) => {
e . preventDefault ( ) ;
let fileMarkdown = ` [ ${ file . name } ](/attachments/ ${ file . uuid } ) ` ;
if ( file . type . startsWith ( 'image/' ) ) {
fileMarkdown = ` ! ${ fileMarkdown } ` ;
} else if ( file . type . startsWith ( 'video/' ) ) {
fileMarkdown = ` <video src="/attachments/ ${ file . uuid } " title=" ${ htmlEscape ( file . name ) } " controls></video> ` ;
2021-10-16 19:28:04 +02:00
}
2024-02-25 07:00:55 +01:00
const success = await clippie ( fileMarkdown ) ;
showTemporaryTooltip ( e . target , success ? i18n . copy _success : i18n . copy _error ) ;
2021-10-16 19:28:04 +02:00
} ) ;
2024-02-25 07:00:55 +01:00
file . previewTemplate . append ( copyLinkElement ) ;
} ) ;
this . on ( 'removedfile' , ( file ) => {
$ ( ` # ${ file . uuid } ` ) . remove ( ) ;
if ( $dropzone . data ( 'remove-url' ) ) {
POST ( $dropzone . data ( 'remove-url' ) , {
data : new URLSearchParams ( { file : file . uuid } ) ,
} ) ;
}
} ) ;
this . on ( 'error' , function ( file , message ) {
showErrorToast ( message ) ;
this . removeFile ( file ) ;
} ) ;
} ,
} ) ;
2021-10-16 19:28:04 +02:00
}
2023-06-19 09:46:50 +02:00
async function linkAction ( e ) {
2023-06-13 14:10:10 +02:00
// A "link-action" can post AJAX request to its "data-url"
2023-06-18 17:23:18 +02:00
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
2023-08-22 04:30:02 +02:00
const el = e . target . closest ( '.link-action' ) ;
if ( ! el ) return ;
2023-06-13 14:10:10 +02:00
2023-08-22 04:30:02 +02:00
e . preventDefault ( ) ;
const url = el . getAttribute ( 'data-url' ) ;
const doRequest = async ( ) => {
el . disabled = true ;
2023-09-19 02:50:30 +02:00
await fetchActionDoRequest ( el , url , { method : 'POST' } ) ;
2023-08-22 04:30:02 +02:00
el . disabled = false ;
2023-06-13 14:10:10 +02:00
} ;
2023-08-22 04:30:02 +02:00
const modalConfirmContent = htmlEscape ( el . getAttribute ( 'data-modal-confirm' ) || '' ) ;
2023-06-19 09:46:50 +02:00
if ( ! modalConfirmContent ) {
2023-08-22 04:30:02 +02:00
await doRequest ( ) ;
2023-06-13 14:10:10 +02:00
return ;
}
2023-08-22 04:30:02 +02:00
const isRisky = el . classList . contains ( 'red' ) || el . classList . contains ( 'yellow' ) || el . classList . contains ( 'orange' ) || el . classList . contains ( 'negative' ) ;
2023-09-19 00:05:31 +02:00
if ( await confirmModal ( { content : modalConfirmContent , buttonColor : isRisky ? 'orange' : 'primary' } ) ) {
2023-08-22 04:30:02 +02:00
await doRequest ( ) ;
2023-06-19 09:46:50 +02:00
}
2023-06-13 14:10:10 +02:00
}
2021-10-16 19:28:04 +02:00
export function initGlobalLinkActions ( ) {
2023-03-14 04:34:09 +01:00
function showDeletePopup ( e ) {
e . preventDefault ( ) ;
2021-10-16 19:28:04 +02:00
const $this = $ ( this ) ;
const dataArray = $this . data ( ) ;
let filter = '' ;
2023-04-18 18:49:49 +02:00
if ( $this . attr ( 'data-modal-id' ) ) {
filter += ` # ${ $this . attr ( 'data-modal-id' ) } ` ;
2021-10-16 19:28:04 +02:00
}
const dialog = $ ( ` .delete.modal ${ filter } ` ) ;
dialog . find ( '.name' ) . text ( $this . data ( 'name' ) ) ;
for ( const [ key , value ] of Object . entries ( dataArray ) ) {
if ( key && key . startsWith ( 'data' ) ) {
dialog . find ( ` . ${ key } ` ) . text ( value ) ;
}
}
dialog . modal ( {
closable : false ,
2024-03-02 09:48:14 +01:00
onApprove : async ( ) => {
2021-10-16 19:28:04 +02:00
if ( $this . data ( 'type' ) === 'form' ) {
2022-01-16 12:19:26 +01:00
$ ( $this . data ( 'form' ) ) . trigger ( 'submit' ) ;
2021-10-16 19:28:04 +02:00
return ;
}
2024-03-02 09:48:14 +01:00
const postData = new FormData ( ) ;
2021-10-16 19:28:04 +02:00
for ( const [ key , value ] of Object . entries ( dataArray ) ) {
if ( key && key . startsWith ( 'data' ) ) {
2024-03-02 09:48:14 +01:00
postData . append ( key . slice ( 4 ) , value ) ;
2021-10-16 19:28:04 +02:00
}
if ( key === 'id' ) {
2024-03-02 09:48:14 +01:00
postData . append ( 'id' , value ) ;
2021-10-16 19:28:04 +02:00
}
}
2024-03-02 09:48:14 +01:00
const response = await POST ( $this . data ( 'url' ) , { data : postData } ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
2021-10-16 19:28:04 +02:00
window . location . href = data . redirect ;
2024-03-02 09:48:14 +01:00
}
2021-10-16 19:28:04 +02:00
}
} ) . modal ( 'show' ) ;
}
// Helpers.
$ ( '.delete-button' ) . on ( 'click' , showDeletePopup ) ;
}
2023-06-21 00:54:15 +02:00
function initGlobalShowModal ( ) {
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
// * First, try to query '#target'
// * Then, try to query '.target'
// * Then, try to query 'target' as HTML tag
// If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
$ ( '.show-modal' ) . on ( 'click' , function ( e ) {
e . preventDefault ( ) ;
const $el = $ ( this ) ;
const modalSelector = $el . attr ( 'data-modal' ) ;
const $modal = $ ( modalSelector ) ;
if ( ! $modal . length ) {
throw new Error ( 'no modal for this action' ) ;
}
const modalAttrPrefix = 'data-modal-' ;
for ( const attrib of this . attributes ) {
if ( ! attrib . name . startsWith ( modalAttrPrefix ) ) {
continue ;
}
const attrTargetCombo = attrib . name . substring ( modalAttrPrefix . length ) ;
const [ attrTargetName , attrTargetAttr ] = attrTargetCombo . split ( '.' ) ;
// try to find target by: "#target" -> ".target" -> "target tag"
let $attrTarget = $modal . find ( ` # ${ attrTargetName } ` ) ;
if ( ! $attrTarget . length ) $attrTarget = $modal . find ( ` . ${ attrTargetName } ` ) ;
if ( ! $attrTarget . length ) $attrTarget = $modal . find ( ` ${ attrTargetName } ` ) ;
if ( ! $attrTarget . length ) continue ; // TODO: show errors in dev mode to remind developers that there is a bug
if ( attrTargetAttr ) {
$attrTarget [ 0 ] [ attrTargetAttr ] = attrib . value ;
} else if ( $attrTarget . is ( 'input' ) || $attrTarget . is ( 'textarea' ) ) {
$attrTarget . val ( attrib . value ) ; // FIXME: add more supports like checkbox
} else {
$attrTarget . text ( attrib . value ) ; // FIXME: it should be more strict here, only handle div/span/p
}
}
const colorPickers = $modal . find ( '.color-picker' ) ;
if ( colorPickers . length > 0 ) {
initCompColorPicker ( ) ; // FIXME: this might cause duplicate init
}
$modal . modal ( 'setting' , {
onApprove : ( ) => {
// "form-fetch-action" can handle network errors gracefully,
// so keep the modal dialog to make users can re-submit the form if anything wrong happens.
if ( $modal . find ( '.form-fetch-action' ) . length ) return false ;
} ,
} ) . modal ( 'show' ) ;
} ) ;
}
2021-10-16 19:28:04 +02:00
export function initGlobalButtons ( ) {
2023-03-14 04:34:09 +01:00
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
2023-03-24 09:37:56 +01:00
$ ( document ) . on ( 'click' , 'form button.ui.cancel.button' , ( e ) => {
2023-03-14 04:34:09 +01:00
e . preventDefault ( ) ;
} ) ;
2023-09-10 12:27:23 +02:00
$ ( '.show-panel' ) . on ( 'click' , function ( e ) {
// a '.show-panel' element can show a panel, by `data-panel="selector"`
// if it has "toggle" class, it toggles the panel
2023-03-14 04:34:09 +01:00
e . preventDefault ( ) ;
2023-03-26 14:06:11 +02:00
const sel = $ ( this ) . attr ( 'data-panel' ) ;
if ( this . classList . contains ( 'toggle' ) ) {
toggleElem ( sel ) ;
} else {
showElem ( sel ) ;
}
2021-10-16 19:28:04 +02:00
} ) ;
2023-09-10 12:27:23 +02:00
$ ( '.hide-panel' ) . on ( 'click' , function ( e ) {
// a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
2023-03-14 04:34:09 +01:00
e . preventDefault ( ) ;
2022-01-07 02:18:52 +01:00
let sel = $ ( this ) . attr ( 'data-panel' ) ;
if ( sel ) {
2023-02-19 05:06:14 +01:00
hideElem ( $ ( sel ) ) ;
2022-01-07 02:18:52 +01:00
return ;
}
sel = $ ( this ) . attr ( 'data-panel-closest' ) ;
if ( sel ) {
2023-02-19 05:06:14 +01:00
hideElem ( $ ( this ) . closest ( sel ) ) ;
2022-01-07 02:18:52 +01:00
return ;
}
// should never happen, otherwise there is a bug in code
2023-06-27 04:45:24 +02:00
showErrorToast ( 'Nothing to hide' ) ;
2021-10-16 19:28:04 +02:00
} ) ;
2023-06-21 00:54:15 +02:00
initGlobalShowModal ( ) ;
2021-10-16 19:28:04 +02:00
}
Show messages for users if the ROOT_URL is wrong, show JavaScript errors (#18971)
* ROOT_URL issues: some users did wrong to there app.ini config, then:
* The assets can not be loaded (AppSubUrl != "" and users try to access http://host:3000/)
*The ROOT_URL is wrong, then many URLs in Gitea are broken.
Now Gitea show enough information to users.
* JavaScript error issues, there are many users affected by JavaScript errors, some are caused by frontend bugs, some are caused by broken customized templates. If these JS errors can be found at first time, then maintainers do not need to ask about how bug occurs again and again.
* Some people like to modify the `head.tmpl`, so we separate the script part to `head_script.tmpl`, then it's much safer.
* use specialized CSS class "js-global-error", end users still have a chance to hide error messages by customized CSS styles.
2022-03-30 07:52:24 +02:00
/ * *
* Too many users set their ROOT _URL to wrong value , and it causes a lot of problems :
* * Cross - origin API request without correct cookie
* * Incorrect href in < a >
* * ...
* So we check whether current URL starts with AppUrl ( ROOT _URL ) .
* If they don ' t match , show a warning to users .
* /
export function checkAppUrl ( ) {
const curUrl = window . location . href ;
2022-07-27 11:19:10 +02:00
// some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
if ( curUrl . startsWith ( appUrl ) || ` ${ curUrl } / ` === appUrl ) {
Show messages for users if the ROOT_URL is wrong, show JavaScript errors (#18971)
* ROOT_URL issues: some users did wrong to there app.ini config, then:
* The assets can not be loaded (AppSubUrl != "" and users try to access http://host:3000/)
*The ROOT_URL is wrong, then many URLs in Gitea are broken.
Now Gitea show enough information to users.
* JavaScript error issues, there are many users affected by JavaScript errors, some are caused by frontend bugs, some are caused by broken customized templates. If these JS errors can be found at first time, then maintainers do not need to ask about how bug occurs again and again.
* Some people like to modify the `head.tmpl`, so we separate the script part to `head_script.tmpl`, then it's much safer.
* use specialized CSS class "js-global-error", end users still have a chance to hide error messages by customized CSS styles.
2022-03-30 07:52:24 +02:00
return ;
}
2023-02-09 17:14:45 +01:00
showGlobalErrorMessage ( ` Your ROOT_URL in app.ini is " ${ appUrl } ", it's unlikely matching the site you are visiting.
2023-07-19 00:14:30 +02:00
Mismatched ROOT _URL config causes wrong URL links for web UI / mail content / webhook notification / OAuth2 sign - in . ` );
Show messages for users if the ROOT_URL is wrong, show JavaScript errors (#18971)
* ROOT_URL issues: some users did wrong to there app.ini config, then:
* The assets can not be loaded (AppSubUrl != "" and users try to access http://host:3000/)
*The ROOT_URL is wrong, then many URLs in Gitea are broken.
Now Gitea show enough information to users.
* JavaScript error issues, there are many users affected by JavaScript errors, some are caused by frontend bugs, some are caused by broken customized templates. If these JS errors can be found at first time, then maintainers do not need to ask about how bug occurs again and again.
* Some people like to modify the `head.tmpl`, so we separate the script part to `head_script.tmpl`, then it's much safer.
* use specialized CSS class "js-global-error", end users still have a chance to hide error messages by customized CSS styles.
2022-03-30 07:52:24 +02:00
}