diff --git a/Makefile b/Makefile index 953abe83b0c8d..6128fb8a4a8f7 100644 --- a/Makefile +++ b/Makefile @@ -418,6 +418,10 @@ npm-check: exit 1; \ fi; +.PHONY: webpack +webpack: npm + npm install webpack -g + .PHONY: npm npm: npm-check npm install --no-save @@ -429,14 +433,15 @@ npm-update: npm-check npm install --package-lock .PHONY: js -js: npm - npx eslint public/js +js: npm webpack + npx eslint web_src/js + webpack .PHONY: css css: npm - npx stylelint public/less - npx lessc --clean-css="--s0 -b" public/less/index.less public/css/index.css - $(foreach file, $(filter-out public/less/themes/_base.less, $(wildcard public/less/themes/*)),npx lessc --clean-css="--s0 -b" public/less/themes/$(notdir $(file)) > public/css/theme-$(notdir $(call strip-suffix,$(file))).css;) + npx stylelint web_src/less + npx lessc --clean-css="--s0 -b" web_src/less/index.less public/css/index.css + $(foreach file, $(filter-out web_src/less/themes/_base.less, $(wildcard web_src/less/themes/*)),npx lessc --clean-css="--s0 -b" web_src/less/themes/$(notdir $(file)) > public/css/theme-$(notdir $(call strip-suffix,$(file))).css;) npx postcss --use autoprefixer --no-map --replace public/css/* @diff=$$(git diff public/css/*); \ diff --git a/public/js/index.js b/public/js/index.js index 8a85ad91579e5..435bd1e7be661 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,3149 +1 @@ -/* globals wipPrefixes, issuesTribute, emojiTribute */ -/* exported timeAddManual, toggleStopwatch, cancelStopwatch, initHeatmap */ -/* exported toggleDeadlineForm, setDeadline, deleteDependencyModal, cancelCodeComment, onOAuthLoginClick */ -'use strict'; - -function htmlEncode(text) { - return jQuery('
').text(text).html() -} - -let csrf; -let suburl; -let previewFileModes; -let simpleMDEditor; -let codeMirrorEditor; - -// Disable Dropzone auto-discover because it's manually initialized -if (typeof(Dropzone) !== "undefined") { - Dropzone.autoDiscover = false; -} - -// Polyfill for IE9+ support (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) -if (!Array.from) { - Array.from = (function () { - const toStr = Object.prototype.toString; - const isCallable = function (fn) { - return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; - }; - const toInteger = function (value) { - const number = Number(value); - if (isNaN(number)) { return 0; } - if (number === 0 || !isFinite(number)) { return number; } - return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); - }; - const maxSafeInteger = Math.pow(2, 53) - 1; - const toLength = function (value) { - const len = toInteger(value); - return Math.min(Math.max(len, 0), maxSafeInteger); - }; - - // The length property of the from method is 1. - return function from(arrayLike/*, mapFn, thisArg */) { - // 1. Let C be the this value. - const C = this; - - // 2. Let items be ToObject(arrayLike). - const items = Object(arrayLike); - - // 3. ReturnIfAbrupt(items). - if (arrayLike == null) { - throw new TypeError("Array.from requires an array-like object - not null or undefined"); - } - - // 4. If mapfn is undefined, then let mapping be false. - const mapFn = arguments.length > 1 ? arguments[1] : void undefined; - let T; - if (typeof mapFn !== 'undefined') { - // 5. else - // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. - if (!isCallable(mapFn)) { - throw new TypeError('Array.from: when provided, the second argument must be a function'); - } - - // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (arguments.length > 2) { - T = arguments[2]; - } - } - - // 10. Let lenValue be Get(items, "length"). - // 11. Let len be ToLength(lenValue). - const len = toLength(items.length); - - // 13. If IsConstructor(C) is true, then - // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len. - // 14. a. Else, Let A be ArrayCreate(len). - const A = isCallable(C) ? Object(new C(len)) : new Array(len); - - // 16. Let k be 0. - let k = 0; - // 17. Repeat, while k < len… (also steps a - h) - let kValue; - while (k < len) { - kValue = items[k]; - if (mapFn) { - A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); - } else { - A[k] = kValue; - } - k += 1; - } - // 18. Let putStatus be Put(A, "length", len, true). - A.length = len; - // 20. Return A. - return A; - }; - }()); -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign -if (typeof Object.assign != 'function') { - // Must be writable: true, enumerable: false, configurable: true - Object.defineProperty(Object, "assign", { - value: function assign(target, _varArgs) { // .length of function is 2 - 'use strict'; - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - const to = Object(target); - - for (let index = 1; index < arguments.length; index++) { - const nextSource = arguments[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (const nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }, - writable: true, - configurable: true - }); -} - -function initCommentPreviewTab($form) { - const $tabMenu = $form.find('.tabular.menu'); - $tabMenu.find('.item').tab(); - $tabMenu.find('.item[data-tab="' + $tabMenu.data('preview') + '"]').click(function () { - const $this = $(this); - $.post($this.data('url'), { - "_csrf": csrf, - "mode": "gfm", - "context": $this.data('context'), - "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val() - }, - function (data) { - const $previewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('preview') + '"]'); - $previewPanel.html(data); - emojify.run($previewPanel[0]); - $('pre code', $previewPanel[0]).each(function () { - hljs.highlightBlock(this); - }); - } - ); - }); - - buttonsClickOnEnter(); -} - -function initEditPreviewTab($form) { - const $tabMenu = $form.find('.tabular.menu'); - $tabMenu.find('.item').tab(); - const $previewTab = $tabMenu.find('.item[data-tab="' + $tabMenu.data('preview') + '"]'); - if ($previewTab.length) { - previewFileModes = $previewTab.data('preview-file-modes').split(','); - $previewTab.click(function () { - const $this = $(this); - $.post($this.data('url'), { - "_csrf": csrf, - "mode": "gfm", - "context": $this.data('context'), - "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val() - }, - function (data) { - const $previewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('preview') + '"]'); - $previewPanel.html(data); - emojify.run($previewPanel[0]); - $('pre code', $previewPanel[0]).each(function () { - hljs.highlightBlock(this); - }); - } - ); - }); - } -} - -function initEditDiffTab($form) { - const $tabMenu = $form.find('.tabular.menu'); - $tabMenu.find('.item').tab(); - $tabMenu.find('.item[data-tab="' + $tabMenu.data('diff') + '"]').click(function () { - const $this = $(this); - $.post($this.data('url'), { - "_csrf": csrf, - "context": $this.data('context'), - "content": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val() - }, - function (data) { - const $diffPreviewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('diff') + '"]'); - $diffPreviewPanel.html(data); - emojify.run($diffPreviewPanel[0]); - } - ); - }); -} - - -function initEditForm() { - if ($('.edit.form').length == 0) { - return; - } - - initEditPreviewTab($('.edit.form')); - initEditDiffTab($('.edit.form')); -} - -function initBranchSelector() { - const $selectBranch = $('.ui.select-branch') - const $branchMenu = $selectBranch.find('.reference-list-menu'); - $branchMenu.find('.item:not(.no-select)').click(function () { - const selectedValue = $(this).data('id'); - $($(this).data('id-selector')).val(selectedValue); - $selectBranch.find('.ui .branch-name').text(selectedValue); - }); - $selectBranch.find('.reference.column').click(function () { - $selectBranch.find('.scrolling.reference-list-menu').css('display', 'none'); - $selectBranch.find('.reference .text').removeClass('black'); - $($(this).data('target')).css('display', 'block'); - $(this).find('.text').addClass('black'); - return false; - }); -} - -function updateIssuesMeta(url, action, issueIds, elementId) { - return new Promise(function(resolve) { - $.ajax({ - type: "POST", - url: url, - data: { - "_csrf": csrf, - "action": action, - "issue_ids": issueIds, - "id": elementId - }, - success: resolve - }) - }) -} - -function initReactionSelector(parent) { - let reactions = ''; - if (!parent) { - parent = $(document); - reactions = '.reactions > '; - } - - parent.find(reactions + 'a.label').popup({'position': 'bottom left', 'metadata': {'content': 'title', 'title': 'none'}}); - - parent.find('.select-reaction > .menu > .item, ' + reactions + 'a.label').on('click', function(e){ - const vm = this; - e.preventDefault(); - - if ($(this).hasClass('disabled')) return; - - const actionURL = $(this).hasClass('item') ? - $(this).closest('.select-reaction').data('action-url') : - $(this).data('action-url'); - const url = actionURL + '/' + ($(this).hasClass('blue') ? 'unreact' : 'react'); - $.ajax({ - type: 'POST', - url: url, - data: { - '_csrf': csrf, - 'content': $(this).data('content') - } - }).done(function(resp) { - if (resp && (resp.html || resp.empty)) { - const content = $(vm).closest('.content'); - let react = content.find('.segment.reactions'); - if (react.length > 0) { - react.remove(); - } - if (!resp.empty) { - react = $(''); - const attachments = content.find('.segment.bottom:first'); - if (attachments.length > 0) { - react.insertBefore(attachments); - } else { - react.appendTo(content); - } - react.html(resp.html); - const hasEmoji = react.find('.has-emoji'); - for (let i = 0; i < hasEmoji.length; i++) { - emojify.run(hasEmoji.get(i)); - } - react.find('.dropdown').dropdown(); - initReactionSelector(react); - } - } - }); - }); -} - -function insertAtCursor(field, value) { - if (field.selectionStart || field.selectionStart === 0) { - const startPos = field.selectionStart; - const endPos = field.selectionEnd; - field.value = field.value.substring(0, startPos) - + value - + field.value.substring(endPos, field.value.length); - field.selectionStart = startPos + value.length; - field.selectionEnd = startPos + value.length; - } else { - field.value += value; - } -} - -function replaceAndKeepCursor(field, oldval, newval) { - if (field.selectionStart || field.selectionStart === 0) { - const startPos = field.selectionStart; - const endPos = field.selectionEnd; - field.value = field.value.replace(oldval, newval); - field.selectionStart = startPos + newval.length - oldval.length; - field.selectionEnd = endPos + newval.length - oldval.length; - } else { - field.value = field.value.replace(oldval, newval); - } -} - -function retrieveImageFromClipboardAsBlob(pasteEvent, callback){ - if (!pasteEvent.clipboardData) { - return; - } - - const items = pasteEvent.clipboardData.items; - if (typeof(items) === "undefined") { - return; - } - - for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") === -1) continue; - const blob = items[i].getAsFile(); - - if (typeof(callback) === "function") { - pasteEvent.preventDefault(); - pasteEvent.stopPropagation(); - callback(blob); - } - } -} - -function uploadFile(file, callback) { - const xhr = new XMLHttpRequest(); - - xhr.onload = function() { - if (xhr.status == 200) { - callback(xhr.responseText); - } - }; - - xhr.open("post", suburl + "/attachments", true); - xhr.setRequestHeader("X-Csrf-Token", csrf); - const formData = new FormData(); - formData.append('file', file, file.name); - xhr.send(formData); -} - -function reload() { - window.location.reload(); -} - -function initImagePaste(target) { - target.each(function() { - const field = this; - field.addEventListener('paste', function(event){ - retrieveImageFromClipboardAsBlob(event, function(img) { - const name = img.name.substr(0, img.name.lastIndexOf('.')); - insertAtCursor(field, '![' + name + ']()'); - uploadFile(img, function(res) { - const data = JSON.parse(res); - replaceAndKeepCursor(field, '![' + name + ']()', ''); - const input = $('').val(data.uuid); - $('.files').append(input); - }); - }); - }, false); - }); -} - -function initCommentForm() { - if ($('.comment.form').length == 0) { - return - } - - initBranchSelector(); - initCommentPreviewTab($('.comment.form')); - initImagePaste($('.comment.form textarea')); - - // Listsubmit - function initListSubmits(selector, outerSelector) { - const $list = $('.ui.' + outerSelector + '.list'); - const $noSelect = $list.find('.no-select'); - const $listMenu = $('.' + selector + ' .menu'); - let hasLabelUpdateAction = $listMenu.data('action') == 'update'; - const labels = {}; - - $('.' + selector).dropdown('setting', 'onHide', function(){ - hasLabelUpdateAction = $listMenu.data('action') == 'update'; // Update the var - if (hasLabelUpdateAction) { - const promises = []; - Object.keys(labels).forEach(function(elementId) { - const label = labels[elementId]; - const promise = updateIssuesMeta( - label["update-url"], - label["action"], - label["issue-id"], - elementId - ); - promises.push(promise); - }); - Promise.all(promises).then(reload); - } - }); - - $listMenu.find('.item:not(.no-select)').click(function () { - - // we don't need the action attribute when updating assignees - if (selector == 'select-assignees-modify') { - - // UI magic. We need to do this here, otherwise it would destroy the functionality of - // adding/removing labels - if ($(this).hasClass('checked')) { - $(this).removeClass('checked'); - $(this).find('.octicon').removeClass('octicon-check'); - } else { - $(this).addClass('checked'); - $(this).find('.octicon').addClass('octicon-check'); - } - - updateIssuesMeta( - $listMenu.data('update-url'), - "", - $listMenu.data('issue-id'), - $(this).data('id') - ); - $listMenu.data('action', 'update'); // Update to reload the page when we updated items - return false; - } - - if ($(this).hasClass('checked')) { - $(this).removeClass('checked'); - $(this).find('.octicon').removeClass('octicon-check'); - if (hasLabelUpdateAction) { - if (!($(this).data('id') in labels)) { - labels[$(this).data('id')] = { - "update-url": $listMenu.data('update-url'), - "action": "detach", - "issue-id": $listMenu.data('issue-id'), - }; - } else { - delete labels[$(this).data('id')]; - } - } - } else { - $(this).addClass('checked'); - $(this).find('.octicon').addClass('octicon-check'); - if (hasLabelUpdateAction) { - if (!($(this).data('id') in labels)) { - labels[$(this).data('id')] = { - "update-url": $listMenu.data('update-url'), - "action": "attach", - "issue-id": $listMenu.data('issue-id'), - }; - } else { - delete labels[$(this).data('id')]; - } - } - } - - const listIds = []; - $(this).parent().find('.item').each(function () { - if ($(this).hasClass('checked')) { - listIds.push($(this).data('id')); - $($(this).data('id-selector')).removeClass('hide'); - } else { - $($(this).data('id-selector')).addClass('hide'); - } - }); - if (listIds.length == 0) { - $noSelect.removeClass('hide'); - } else { - $noSelect.addClass('hide'); - } - $($(this).parent().data('id')).val(listIds.join(",")); - return false; - }); - $listMenu.find('.no-select.item').click(function () { - if (hasLabelUpdateAction || selector == 'select-assignees-modify') { - updateIssuesMeta( - $listMenu.data('update-url'), - "clear", - $listMenu.data('issue-id'), - "" - ).then(reload); - } - - $(this).parent().find('.item').each(function () { - $(this).removeClass('checked'); - $(this).find('.octicon').removeClass('octicon-check'); - }); - - $list.find('.item').each(function () { - $(this).addClass('hide'); - }); - $noSelect.removeClass('hide'); - $($(this).parent().data('id')).val(''); - - }); - } - - // Init labels and assignees - initListSubmits('select-label', 'labels'); - initListSubmits('select-assignees', 'assignees'); - initListSubmits('select-assignees-modify', 'assignees'); - - function selectItem(select_id, input_id) { - const $menu = $(select_id + ' .menu'); - const $list = $('.ui' + select_id + '.list'); - const hasUpdateAction = $menu.data('action') == 'update'; - - $menu.find('.item:not(.no-select)').click(function () { - $(this).parent().find('.item').each(function () { - $(this).removeClass('selected active') - }); - - $(this).addClass('selected active'); - if (hasUpdateAction) { - updateIssuesMeta( - $menu.data('update-url'), - "", - $menu.data('issue-id'), - $(this).data('id') - ).then(reload); - } - switch (input_id) { - case '#milestone_id': - $list.find('.selected').html('' + - htmlEncode($(this).text()) + ''); - break; - case '#assignee_id': - $list.find('.selected').html('' + - '