You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

common-global.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import $ from 'jquery';
  2. import 'jquery.are-you-sure';
  3. import {mqBinarySearch} from '../utils.js';
  4. import {createDropzone} from './dropzone.js';
  5. import {initCompColorPicker} from './comp/ColorPicker.js';
  6. import {showGlobalErrorMessage} from '../bootstrap.js';
  7. import {attachCheckboxAria, attachDropdownAria} from './aria.js';
  8. import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
  9. import {initTooltip} from '../modules/tippy.js';
  10. import {svg} from '../svg.js';
  11. import {hideElem, showElem, toggleElem} from '../utils/dom.js';
  12. const {appUrl, csrfToken} = window.config;
  13. export function initGlobalFormDirtyLeaveConfirm() {
  14. // Warn users that try to leave a page after entering data into a form.
  15. // Except on sign-in pages, and for forms marked as 'ignore-dirty'.
  16. if ($('.user.signin').length === 0) {
  17. $('form:not(.ignore-dirty)').areYouSure();
  18. }
  19. }
  20. export function initHeadNavbarContentToggle() {
  21. const content = $('#navbar');
  22. const toggle = $('#navbar-expand-toggle');
  23. let isExpanded = false;
  24. toggle.on('click', () => {
  25. isExpanded = !isExpanded;
  26. if (isExpanded) {
  27. content.addClass('shown');
  28. toggle.addClass('active');
  29. } else {
  30. content.removeClass('shown');
  31. toggle.removeClass('active');
  32. }
  33. });
  34. }
  35. export function initFootLanguageMenu() {
  36. function linkLanguageAction() {
  37. const $this = $(this);
  38. $.get($this.data('url')).always(() => {
  39. window.location.reload();
  40. });
  41. }
  42. $('.language-menu a[lang]').on('click', linkLanguageAction);
  43. }
  44. export function initGlobalEnterQuickSubmit() {
  45. $(document).on('keydown', '.js-quick-submit', (e) => {
  46. if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter')) {
  47. handleGlobalEnterQuickSubmit(e.target);
  48. return false;
  49. }
  50. });
  51. }
  52. export function initGlobalButtonClickOnEnter() {
  53. $(document).on('keypress', '.ui.button', (e) => {
  54. if (e.keyCode === 13 || e.keyCode === 32) { // enter key or space bar
  55. $(e.target).trigger('click');
  56. e.preventDefault();
  57. }
  58. });
  59. }
  60. export function initGlobalTooltips() {
  61. for (const el of document.getElementsByClassName('tooltip')) {
  62. initTooltip(el);
  63. }
  64. }
  65. export function initGlobalCommon() {
  66. // Undo Safari emoji glitch fix at high enough zoom levels
  67. if (navigator.userAgent.match('Safari')) {
  68. $(window).resize(() => {
  69. const px = mqBinarySearch('width', 0, 4096, 1, 'px');
  70. const em = mqBinarySearch('width', 0, 1024, 0.01, 'em');
  71. if (em * 16 * 1.25 - px <= -1) {
  72. $('body').addClass('safari-above125');
  73. } else {
  74. $('body').removeClass('safari-above125');
  75. }
  76. });
  77. }
  78. // Semantic UI modules.
  79. const $uiDropdowns = $('.ui.dropdown');
  80. $uiDropdowns.filter(':not(.custom)').dropdown({
  81. fullTextSearch: 'exact'
  82. });
  83. $uiDropdowns.filter('.jump').dropdown({
  84. action: 'hide',
  85. onShow() {
  86. // hide associated tooltip while dropdown is open
  87. this._tippy?.hide();
  88. this._tippy?.disable();
  89. },
  90. onHide() {
  91. this._tippy?.enable();
  92. },
  93. fullTextSearch: 'exact'
  94. });
  95. $uiDropdowns.filter('.slide.up').dropdown({
  96. transition: 'slide up',
  97. fullTextSearch: 'exact'
  98. });
  99. $uiDropdowns.filter('.upward').dropdown({
  100. direction: 'upward',
  101. fullTextSearch: 'exact'
  102. });
  103. attachDropdownAria($uiDropdowns);
  104. attachCheckboxAria($('.ui.checkbox'));
  105. $('.tabular.menu .item').tab();
  106. $('.tabable.menu .item').tab();
  107. $('.toggle.button').on('click', function () {
  108. toggleElem($($(this).data('target')));
  109. });
  110. // make table <tr> and <td> elements clickable like a link
  111. $('tr[data-href], td[data-href]').on('click', function (e) {
  112. const href = $(this).data('href');
  113. if (e.target.nodeName === 'A') {
  114. // if a user clicks on <a>, then the <tr> or <td> should not act as a link.
  115. return;
  116. }
  117. if (e.ctrlKey || e.metaKey) {
  118. // ctrl+click or meta+click opens a new window in modern browsers
  119. window.open(href);
  120. } else {
  121. window.location = href;
  122. }
  123. });
  124. // prevent multiple form submissions on forms containing .loading-button
  125. document.addEventListener('submit', (e) => {
  126. const btn = e.target.querySelector('.loading-button');
  127. if (!btn) return;
  128. if (btn.classList.contains('loading')) return e.preventDefault();
  129. btn.classList.add('loading');
  130. });
  131. }
  132. export function initGlobalDropzone() {
  133. // Dropzone
  134. for (const el of document.querySelectorAll('.dropzone')) {
  135. const $dropzone = $(el);
  136. const _promise = createDropzone(el, {
  137. url: $dropzone.data('upload-url'),
  138. headers: {'X-Csrf-Token': csrfToken},
  139. maxFiles: $dropzone.data('max-file'),
  140. maxFilesize: $dropzone.data('max-size'),
  141. acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
  142. addRemoveLinks: true,
  143. dictDefaultMessage: $dropzone.data('default-message'),
  144. dictInvalidFileType: $dropzone.data('invalid-input-type'),
  145. dictFileTooBig: $dropzone.data('file-too-big'),
  146. dictRemoveFile: $dropzone.data('remove-file'),
  147. timeout: 0,
  148. thumbnailMethod: 'contain',
  149. thumbnailWidth: 480,
  150. thumbnailHeight: 480,
  151. init() {
  152. this.on('success', (file, data) => {
  153. file.uuid = data.uuid;
  154. const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
  155. $dropzone.find('.files').append(input);
  156. // Create a "Copy Link" element, to conveniently copy the image
  157. // or file link as Markdown to the clipboard
  158. const copyLinkElement = document.createElement('div');
  159. copyLinkElement.className = 'gt-tc';
  160. // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
  161. copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
  162. copyLinkElement.addEventListener('click', (e) => {
  163. e.preventDefault();
  164. let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
  165. if (file.type.startsWith('image/')) {
  166. fileMarkdown = `!${fileMarkdown}`;
  167. }
  168. navigator.clipboard.writeText(fileMarkdown);
  169. });
  170. file.previewTemplate.appendChild(copyLinkElement);
  171. });
  172. this.on('removedfile', (file) => {
  173. $(`#${file.uuid}`).remove();
  174. if ($dropzone.data('remove-url')) {
  175. $.post($dropzone.data('remove-url'), {
  176. file: file.uuid,
  177. _csrf: csrfToken,
  178. });
  179. }
  180. });
  181. },
  182. });
  183. }
  184. }
  185. export function initGlobalLinkActions() {
  186. function showDeletePopup() {
  187. const $this = $(this);
  188. const dataArray = $this.data();
  189. let filter = '';
  190. if ($this.data('modal-id')) {
  191. filter += `#${$this.data('modal-id')}`;
  192. }
  193. const dialog = $(`.delete.modal${filter}`);
  194. dialog.find('.name').text($this.data('name'));
  195. for (const [key, value] of Object.entries(dataArray)) {
  196. if (key && key.startsWith('data')) {
  197. dialog.find(`.${key}`).text(value);
  198. }
  199. }
  200. dialog.modal({
  201. closable: false,
  202. onApprove() {
  203. if ($this.data('type') === 'form') {
  204. $($this.data('form')).trigger('submit');
  205. return;
  206. }
  207. const postData = {
  208. _csrf: csrfToken,
  209. };
  210. for (const [key, value] of Object.entries(dataArray)) {
  211. if (key && key.startsWith('data')) {
  212. postData[key.slice(4)] = value;
  213. }
  214. if (key === 'id') {
  215. postData['id'] = value;
  216. }
  217. }
  218. $.post($this.data('url'), postData).done((data) => {
  219. window.location.href = data.redirect;
  220. });
  221. }
  222. }).modal('show');
  223. return false;
  224. }
  225. function showAddAllPopup() {
  226. const $this = $(this);
  227. let filter = '';
  228. if ($this.attr('id')) {
  229. filter += `#${$this.attr('id')}`;
  230. }
  231. const dialog = $(`.addall.modal${filter}`);
  232. dialog.find('.name').text($this.data('name'));
  233. dialog.modal({
  234. closable: false,
  235. onApprove() {
  236. if ($this.data('type') === 'form') {
  237. $($this.data('form')).trigger('submit');
  238. return;
  239. }
  240. $.post($this.data('url'), {
  241. _csrf: csrfToken,
  242. id: $this.data('id')
  243. }).done((data) => {
  244. window.location.href = data.redirect;
  245. });
  246. }
  247. }).modal('show');
  248. return false;
  249. }
  250. function linkAction(e) {
  251. e.preventDefault();
  252. const $this = $(this);
  253. const redirect = $this.data('redirect');
  254. $this.prop('disabled', true);
  255. $.post($this.data('url'), {
  256. _csrf: csrfToken
  257. }).done((data) => {
  258. if (data.redirect) {
  259. window.location.href = data.redirect;
  260. } else if (redirect) {
  261. window.location.href = redirect;
  262. } else {
  263. window.location.reload();
  264. }
  265. }).always(() => {
  266. $this.prop('disabled', false);
  267. });
  268. }
  269. // Helpers.
  270. $('.delete-button').on('click', showDeletePopup);
  271. $('.link-action').on('click', linkAction);
  272. // FIXME: this function is only used once, and not common, not well designed. should be refactored later
  273. $('.add-all-button').on('click', showAddAllPopup);
  274. // FIXME: this is only used once, and should be replace with `link-action` instead
  275. $('.undo-button').on('click', function () {
  276. const $this = $(this);
  277. $this.prop('disabled', true);
  278. $.post($this.data('url'), {
  279. _csrf: csrfToken,
  280. id: $this.data('id')
  281. }).done((data) => {
  282. window.location.href = data.redirect;
  283. }).always(() => {
  284. $this.prop('disabled', false);
  285. });
  286. });
  287. }
  288. export function initGlobalButtons() {
  289. $('.show-panel.button').on('click', function () {
  290. showElem($(this).data('panel'));
  291. });
  292. $('.hide-panel.button').on('click', function (event) {
  293. // a `.hide-panel.button` can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
  294. event.preventDefault();
  295. let sel = $(this).attr('data-panel');
  296. if (sel) {
  297. hideElem($(sel));
  298. return;
  299. }
  300. sel = $(this).attr('data-panel-closest');
  301. if (sel) {
  302. hideElem($(this).closest(sel));
  303. return;
  304. }
  305. // should never happen, otherwise there is a bug in code
  306. alert('Nothing to hide');
  307. });
  308. $('.show-modal').on('click', function () {
  309. const modalDiv = $($(this).attr('data-modal'));
  310. for (const attrib of this.attributes) {
  311. if (!attrib.name.startsWith('data-modal-')) {
  312. continue;
  313. }
  314. const id = attrib.name.substring(11);
  315. const target = modalDiv.find(`#${id}`);
  316. if (target.is('input')) {
  317. target.val(attrib.value);
  318. } else {
  319. target.text(attrib.value);
  320. }
  321. }
  322. modalDiv.modal('show');
  323. const colorPickers = $($(this).attr('data-modal')).find('.color-picker');
  324. if (colorPickers.length > 0) {
  325. initCompColorPicker();
  326. }
  327. });
  328. $('.delete-post.button').on('click', function () {
  329. const $this = $(this);
  330. $.post($this.attr('data-request-url'), {
  331. _csrf: csrfToken
  332. }).done(() => {
  333. window.location.href = $this.attr('data-done-url');
  334. });
  335. });
  336. }
  337. /**
  338. * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
  339. * * Cross-origin API request without correct cookie
  340. * * Incorrect href in <a>
  341. * * ...
  342. * So we check whether current URL starts with AppUrl(ROOT_URL).
  343. * If they don't match, show a warning to users.
  344. */
  345. export function checkAppUrl() {
  346. const curUrl = window.location.href;
  347. // some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
  348. if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) {
  349. return;
  350. }
  351. showGlobalErrorMessage(`Your ROOT_URL in app.ini is "${appUrl}", it's unlikely matching the site you are visiting.
  352. Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification.`);
  353. }