Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

common-global.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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. const {appUrl, csrfToken} = window.config;
  8. export function initGlobalFormDirtyLeaveConfirm() {
  9. // Warn users that try to leave a page after entering data into a form.
  10. // Except on sign-in pages, and for forms marked as 'ignore-dirty'.
  11. if ($('.user.signin').length === 0) {
  12. $('form:not(.ignore-dirty)').areYouSure();
  13. }
  14. }
  15. export function initHeadNavbarContentToggle() {
  16. const content = $('#navbar');
  17. const toggle = $('#navbar-expand-toggle');
  18. let isExpanded = false;
  19. toggle.on('click', () => {
  20. isExpanded = !isExpanded;
  21. if (isExpanded) {
  22. content.addClass('shown');
  23. toggle.addClass('active');
  24. } else {
  25. content.removeClass('shown');
  26. toggle.removeClass('active');
  27. }
  28. });
  29. }
  30. export function initFootLanguageMenu() {
  31. function linkLanguageAction() {
  32. const $this = $(this);
  33. $.post($this.data('url')).always(() => {
  34. window.location.reload();
  35. });
  36. }
  37. $('.language-menu a[lang]').on('click', linkLanguageAction);
  38. }
  39. export function initGlobalEnterQuickSubmit() {
  40. $(document).on('keydown', '.js-quick-submit', (e) => {
  41. if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter')) {
  42. handleGlobalEnterQuickSubmit(e.target);
  43. return false;
  44. }
  45. });
  46. }
  47. export function handleGlobalEnterQuickSubmit(target) {
  48. const $target = $(target);
  49. const $form = $(target).closest('form');
  50. if ($form.length) {
  51. // here use the event to trigger the submit event (instead of calling `submit()` method directly)
  52. // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
  53. $form.trigger('submit');
  54. } else {
  55. // if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
  56. // the 'ce-' prefix means this is a CustomEvent
  57. $target.trigger('ce-quick-submit');
  58. }
  59. }
  60. export function initGlobalButtonClickOnEnter() {
  61. $(document).on('keypress', '.ui.button', (e) => {
  62. if (e.keyCode === 13 || e.keyCode === 32) { // enter key or space bar
  63. $(e.target).trigger('click');
  64. }
  65. });
  66. }
  67. export function initGlobalCommon() {
  68. // Show exact time
  69. $('.time-since').each(function () {
  70. $(this)
  71. .addClass('tooltip')
  72. .attr('data-content', $(this).attr('title'))
  73. .attr('title', '');
  74. });
  75. // Undo Safari emoji glitch fix at high enough zoom levels
  76. if (navigator.userAgent.match('Safari')) {
  77. $(window).resize(() => {
  78. const px = mqBinarySearch('width', 0, 4096, 1, 'px');
  79. const em = mqBinarySearch('width', 0, 1024, 0.01, 'em');
  80. if (em * 16 * 1.25 - px <= -1) {
  81. $('body').addClass('safari-above125');
  82. } else {
  83. $('body').removeClass('safari-above125');
  84. }
  85. });
  86. }
  87. // Semantic UI modules.
  88. $('.dropdown:not(.custom)').dropdown({
  89. fullTextSearch: 'exact'
  90. });
  91. $('.jump.dropdown').dropdown({
  92. action: 'hide',
  93. onShow() {
  94. $('.tooltip').popup('hide');
  95. },
  96. fullTextSearch: 'exact'
  97. });
  98. $('.slide.up.dropdown').dropdown({
  99. transition: 'slide up',
  100. fullTextSearch: 'exact'
  101. });
  102. $('.upward.dropdown').dropdown({
  103. direction: 'upward',
  104. fullTextSearch: 'exact'
  105. });
  106. $('.ui.checkbox').checkbox();
  107. $('.ui.progress').progress({
  108. showActivity: false
  109. });
  110. // init popups
  111. $('.tooltip').each((_, el) => {
  112. const $el = $(el);
  113. const attr = $el.attr('data-variation');
  114. const attrs = attr ? attr.split(' ') : [];
  115. const variations = new Set([...attrs, 'inverted', 'tiny']);
  116. $el.attr('data-variation', [...variations].join(' ')).popup();
  117. });
  118. $('.top.menu .tooltip').popup({
  119. onShow() {
  120. if ($('.top.menu .menu.transition').hasClass('visible')) {
  121. return false;
  122. }
  123. }
  124. });
  125. $('.tabular.menu .item').tab();
  126. $('.tabable.menu .item').tab();
  127. $('.toggle.button').on('click', function () {
  128. $($(this).data('target')).slideToggle(100);
  129. });
  130. // make table <tr> and <td> elements clickable like a link
  131. $('tr[data-href], td[data-href]').on('click', function (e) {
  132. const href = $(this).data('href');
  133. if (e.target.nodeName === 'A') {
  134. // if a user clicks on <a>, then the <tr> or <td> should not act as a link.
  135. return;
  136. }
  137. if (e.ctrlKey || e.metaKey) {
  138. // ctrl+click or meta+click opens a new window in modern browsers
  139. window.open(href);
  140. } else {
  141. window.location = href;
  142. }
  143. });
  144. // loading-button this logic used to prevent push one form more than one time
  145. $(document).on('click', '.button.loading-button', function (e) {
  146. const $btn = $(this);
  147. if ($btn.hasClass('loading')) {
  148. e.preventDefault();
  149. return false;
  150. }
  151. $btn.addClass('loading disabled');
  152. });
  153. }
  154. export function initGlobalDropzone() {
  155. // Dropzone
  156. for (const el of document.querySelectorAll('.dropzone')) {
  157. const $dropzone = $(el);
  158. const _promise = createDropzone(el, {
  159. url: $dropzone.data('upload-url'),
  160. headers: {'X-Csrf-Token': csrfToken},
  161. maxFiles: $dropzone.data('max-file'),
  162. maxFilesize: $dropzone.data('max-size'),
  163. acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
  164. addRemoveLinks: true,
  165. dictDefaultMessage: $dropzone.data('default-message'),
  166. dictInvalidFileType: $dropzone.data('invalid-input-type'),
  167. dictFileTooBig: $dropzone.data('file-too-big'),
  168. dictRemoveFile: $dropzone.data('remove-file'),
  169. timeout: 0,
  170. thumbnailMethod: 'contain',
  171. thumbnailWidth: 480,
  172. thumbnailHeight: 480,
  173. init() {
  174. this.on('success', (_file, data) => {
  175. const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
  176. $dropzone.find('.files').append(input);
  177. });
  178. this.on('removedfile', (file) => {
  179. $(`#${file.uuid}`).remove();
  180. if ($dropzone.data('remove-url')) {
  181. $.post($dropzone.data('remove-url'), {
  182. file: file.uuid,
  183. _csrf: csrfToken,
  184. });
  185. }
  186. });
  187. },
  188. });
  189. }
  190. }
  191. export function initGlobalLinkActions() {
  192. function showDeletePopup() {
  193. const $this = $(this);
  194. const dataArray = $this.data();
  195. let filter = '';
  196. if ($this.data('modal-id')) {
  197. filter += `#${$this.data('modal-id')}`;
  198. }
  199. const dialog = $(`.delete.modal${filter}`);
  200. dialog.find('.name').text($this.data('name'));
  201. for (const [key, value] of Object.entries(dataArray)) {
  202. if (key && key.startsWith('data')) {
  203. dialog.find(`.${key}`).text(value);
  204. }
  205. }
  206. dialog.modal({
  207. closable: false,
  208. onApprove() {
  209. if ($this.data('type') === 'form') {
  210. $($this.data('form')).trigger('submit');
  211. return;
  212. }
  213. const postData = {
  214. _csrf: csrfToken,
  215. };
  216. for (const [key, value] of Object.entries(dataArray)) {
  217. if (key && key.startsWith('data')) {
  218. postData[key.slice(4)] = value;
  219. }
  220. if (key === 'id') {
  221. postData['id'] = value;
  222. }
  223. }
  224. $.post($this.data('url'), postData).done((data) => {
  225. window.location.href = data.redirect;
  226. });
  227. }
  228. }).modal('show');
  229. return false;
  230. }
  231. function showAddAllPopup() {
  232. const $this = $(this);
  233. let filter = '';
  234. if ($this.attr('id')) {
  235. filter += `#${$this.attr('id')}`;
  236. }
  237. const dialog = $(`.addall.modal${filter}`);
  238. dialog.find('.name').text($this.data('name'));
  239. dialog.modal({
  240. closable: false,
  241. onApprove() {
  242. if ($this.data('type') === 'form') {
  243. $($this.data('form')).trigger('submit');
  244. return;
  245. }
  246. $.post($this.data('url'), {
  247. _csrf: csrfToken,
  248. id: $this.data('id')
  249. }).done((data) => {
  250. window.location.href = data.redirect;
  251. });
  252. }
  253. }).modal('show');
  254. return false;
  255. }
  256. function linkAction(e) {
  257. e.preventDefault();
  258. const $this = $(this);
  259. const redirect = $this.data('redirect');
  260. $.post($this.data('url'), {
  261. _csrf: csrfToken
  262. }).done((data) => {
  263. if (data.redirect) {
  264. window.location.href = data.redirect;
  265. } else if (redirect) {
  266. window.location.href = redirect;
  267. } else {
  268. window.location.reload();
  269. }
  270. });
  271. }
  272. // Helpers.
  273. $('.delete-button').on('click', showDeletePopup);
  274. $('.link-action').on('click', linkAction);
  275. // FIXME: this function is only used once, and not common, not well designed. should be refactored later
  276. $('.add-all-button').on('click', showAddAllPopup);
  277. // FIXME: this is only used once, and should be replace with `link-action` instead
  278. $('.undo-button').on('click', function () {
  279. const $this = $(this);
  280. $.post($this.data('url'), {
  281. _csrf: csrfToken,
  282. id: $this.data('id')
  283. }).done((data) => {
  284. window.location.href = data.redirect;
  285. });
  286. });
  287. }
  288. export function initGlobalButtons() {
  289. $('.show-panel.button').on('click', function () {
  290. $($(this).data('panel')).show();
  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. $(sel).hide();
  298. return;
  299. }
  300. sel = $(this).attr('data-panel-closest');
  301. if (sel) {
  302. $(this).closest(sel).hide();
  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. if (curUrl.startsWith(appUrl)) {
  348. return;
  349. }
  350. if (document.querySelector('.page-content.install')) {
  351. return; // no need to show the message on the installation page
  352. }
  353. showGlobalErrorMessage(`Your ROOT_URL in app.ini is ${appUrl} but you are visiting ${curUrl}
  354. You should set ROOT_URL correctly, otherwise the web may not work correctly.`);
  355. }