diff options
Diffstat (limited to 'app/assets/javascripts')
75 files changed, 760 insertions, 62 deletions
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js index 151455ce4a3..d4a4c7abaa1 100644 --- a/app/assets/javascripts/LabelManager.js +++ b/app/assets/javascripts/LabelManager.js @@ -3,6 +3,7 @@ LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time'; function LabelManager(opts) { + // Defaults var ref, ref1, ref2; if (opts == null) { opts = {}; @@ -28,6 +29,7 @@ $btn = $(e.currentTarget); $label = $("#" + ($btn.data('domId'))); action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + // Make sure tooltip will hide $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby'))); $tooltip.tooltip('destroy'); return _this.toggleLabelPriority($label, action); @@ -42,6 +44,7 @@ url = $label.find('.js-toggle-priority').data('url'); $target = this.prioritizedLabels; $from = this.otherLabels; + // Optimistic update if (action === 'remove') { $target = this.otherLabels; $from = this.prioritizedLabels; @@ -53,6 +56,7 @@ $target.find('.empty-message').addClass('hidden'); } $label.detach().appendTo($target); + // Return if we are not persisting state if (!persistState) { return; } @@ -61,6 +65,7 @@ url: url, type: 'DELETE' }); + // Restore empty message if (!$from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 84b292e59c6..6df2ecf57a2 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -24,6 +24,8 @@ return callback(group); }); }, + // Return groups list. Filtered by query + // Only active groups retrieved groups: function(query, skip_ldap, callback) { var url = Api.buildUrl(Api.groupsPath); return $.ajax({ @@ -38,6 +40,7 @@ return callback(groups); }); }, + // Return namespaces list. Filtered by query namespaces: function(query, callback) { var url = Api.buildUrl(Api.namespacesPath); return $.ajax({ @@ -52,6 +55,7 @@ return callback(namespaces); }); }, + // Return projects list. Filtered by query projects: function(query, order, callback) { var url = Api.buildUrl(Api.projectsPath); return $.ajax({ @@ -82,6 +86,7 @@ return callback(message.responseJSON); }); }, + // Return group projects list. Filtered by query groupProjects: function(group_id, query, callback) { var url = Api.buildUrl(Api.groupProjectsPath) .replace(':id', group_id); @@ -97,6 +102,7 @@ return callback(projects); }); }, + // Return text for a specific license licenseText: function(key, data, callback) { var url = Api.buildUrl(Api.licensePath) .replace(':key', key); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index fea6f41d5e9..31fa508d6c1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,3 +1,9 @@ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// /*= require jquery2 */ /*= require jquery-ui/autocomplete */ /*= require jquery-ui/datepicker */ @@ -76,6 +82,7 @@ } }; + // Disable button if text field is empty window.disableButtonIfEmptyField = function(field_selector, button_selector) { var closest_submit, field; field = $(field_selector); @@ -92,6 +99,7 @@ }); }; + // Disable button if any input field with given selector is empty window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) { var closest_submit, updateButtons; closest_submit = form.find(button_selector); @@ -128,6 +136,8 @@ window.addEventListener("hashchange", shiftWindow); window.onload = function() { + // Scroll the window to avoid the topnav bar + // https://github.com/twitter/bootstrap/issues/1768 if (location.hash) { return setTimeout(shiftWindow, 100); } @@ -149,6 +159,8 @@ return $(this).select().one('mouseup', function(e) { return e.preventDefault(); }); + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input }); $('.remove-row').bind('ajax:success', function() { $(this).tooltip('destroy') @@ -163,6 +175,7 @@ }); $('select.select2').select2({ width: 'resolve', + // Initialize select2 selects dropdownAutoWidth: true }); $('.js-select2').bind('select2-close', function() { @@ -170,7 +183,9 @@ $('.select2-container-active').removeClass('select2-container-active'); return $(':focus').blur(); }), 1); + // Close select2 on escape }); + // Initialize tooltips $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', placement: function(_, el) { @@ -179,14 +194,17 @@ }); $('.trigger-submit').on('change', function() { return $(this).parents('form').submit(); + // Form submitter }); gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Flash if ((flash = $(".flash-container")).length > 0) { flash.click(function() { return $(this).fadeOut(); }); flash.show(); } + // Disable form buttons while a form is submitting $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) { var buttons; buttons = $('[type="submit"]', this); @@ -207,6 +225,7 @@ } }); $('.account-box').hover(function() { + // Show/Hide the profile menu when hovering the account box return $(this).toggleClass('hover'); }); $document.on('click', '.diff-content .js-show-suppressed-diff', function() { @@ -214,6 +233,7 @@ $container = $(this).parent(); $container.next('table').show(); return $container.remove(); + // Commit show suppressed diff }); $('.navbar-toggle').on('click', function() { $('.header-content .title').toggle(); @@ -221,6 +241,7 @@ $('.header-content .navbar-collapse').toggle(); return $('.navbar-toggle').toggleClass('active'); }); + // Show/hide comments on diff $body.on("click", ".js-toggle-diff-comments", function(e) { var $this = $(this); $this.toggleClass('active'); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 7116512d6b7..a9aec6e8ea4 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -16,7 +16,7 @@ } Autosave.prototype.restore = function() { - var e, error, text; + var e, text; if (window.localStorage == null) { return; } @@ -41,7 +41,7 @@ if ((text != null ? text.length : void 0) > 0) { try { return window.localStorage.setItem(this.key, text); - } catch (undefined) {} + } catch (error) {} } else { return this.reset(); } @@ -53,7 +53,7 @@ } try { return window.localStorage.removeItem(this.key); - } catch (undefined) {} + } catch (error) {} }; return Autosave; diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 5ea18ea8b7a..0decc6d09e6 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -86,6 +86,8 @@ AwardsHandler.prototype.positionMenu = function($menu, $addBtn) { var css, position; position = $addBtn.data('position'); + // The menu could potentially be off-screen or in a hidden overflow element + // So we position the element absolute in the body css = { top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" }; @@ -284,6 +286,7 @@ if (emojiIcon.length > 0) { unicodeName = emojiIcon.data('unicode-name'); } else { + // Find by alias unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name'); } return "emoji-" + unicodeName; @@ -350,8 +353,10 @@ return function(ev) { var found_emojis, h5, term, ul; term = $(ev.target).val(); + // Clean previous search results $('ul.emoji-menu-search, h5.emoji-search').remove(); if (term) { + // Generate a search result block h5 = $('<h5>').text('Search results'); found_emojis = _this.searchEmojis(term).show(); ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index f977a1e8a7b..dc8ae601961 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,7 +1,5 @@ /*= require jquery.ba-resize */ - - /*= require autosize */ (function() { diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 3631d1b74ac..1df681a4816 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -5,6 +5,12 @@ container = $(this).closest(".js-details-container"); return container.toggleClass("open"); }); + // Show details content. Hides link after click. + // + // %div + // %a.js-details-expand + // %div.js-details-content + // return $("body").on("click", ".js-details-expand", function(e) { $(this).next('.js-details-content').removeClass("hide"); $(this).hide(); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 3527d0a95fc..54b7360ab41 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,6 +1,20 @@ - +// Quick Submit behavior +// +// When a child field of a form with a `js-quick-submit` class receives a +// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form +// is submitted. +// /*= require extensions/jquery */ +// +// ### Example Markup +// +// <form action="/foo" class="js-quick-submit"> +// <input type="text" /> +// <textarea></textarea> +// <input type="submit" value="Submit" /> +// </form> +// (function() { var isMac, keyCodeIs; @@ -17,6 +31,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', function(e) { var $form, $submit_button; + // Enter if (!keyCodeIs(e, 13)) { return; } @@ -33,8 +48,11 @@ return $form.submit(); }); + // If the user tabs to a submit button on a `js-quick-submit` form, display a + // tooltip to let them know they could've used the hotkey $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) { var $this, title; + // Tab if (!keyCodeIs(e, 9)) { return; } diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index db0b36b24e9..894034bdd54 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,6 +1,18 @@ - +// Requires Input behavior +// +// When called on a form with input fields with the `required` attribute, the +// form's submit button will be disabled until all required fields have values. +// /*= require extensions/jquery */ +// +// ### Example Markup +// +// <form class="js-requires-input"> +// <input type="text" required="required"> +// <input type="submit" value="Submit"> +// </form> +// (function() { $.fn.requiresInput = function() { var $button, $form, fieldSelector, requireInput, required; @@ -11,14 +23,17 @@ requireInput = function() { var values; values = _.map($(fieldSelector, $form), function(field) { + // Collect the input values of *all* required fields return field.value; }); + // Disable the button if any required fields are empty if (values.length && _.any(values, _.isEmpty)) { return $button.disable(); } else { return $button.enable(); } }; + // Set initial button state requireInput(); return $form.on('change input', fieldSelector, requireInput); }; @@ -27,6 +42,8 @@ var $form, hideOrShowHelpBlock; $form = $('form.js-requires-input'); $form.requiresInput(); + // Hide or Show the help block when creating a new project + // based on the option selected hideOrShowHelpBlock = function(form) { var selected; selected = $('.js-select-namespace option:selected'); diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 5467e3edc69..a6ce378d67a 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,5 +1,12 @@ (function(w) { $(function() { + // Toggle button. Show/hide content inside parent container. + // Button does not change visibility. If button has icon - it changes chevron style. + // + // %div.js-toggle-container + // %a.js-toggle-button + // %div.js-toggle-content + // $('body').on('click', '.js-toggle-button', function(e) { e.preventDefault(); $(this) diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index f4044f22db2..8cca1aa9232 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -8,6 +8,8 @@ autoDiscover: false, autoProcessQueue: false, url: form.attr('action'), + // Rails uses a hidden input field for PUT + // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails method: method, clickable: true, uploadMultiple: false, @@ -36,6 +38,7 @@ formData.append('commit_message', form.find('.js-commit-message').val()); }); }, + // Override behavior of adding error underneath preview error: function(file, errorMessage) { var stripped; stripped = $("<div/>").html(errorMessage).text(); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index b0a37ef0e0a..b18b6962382 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -66,6 +66,9 @@ // be added by all subclasses. }; + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { this.editor.setValue(file.content, 1); if (!skipFocus) this.editor.focus(); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 649c79daee8..b846bab0424 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -18,6 +18,8 @@ return function() { return $("#file-content").val(_this.editor.getValue()); }; + // Before a form submission, move the content from the Ace editor into the + // submitted textarea })(this)); this.initModePanesAndLinks(); new BlobLicenseSelectors({ diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index 1e0148e5798..5fef9725178 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -23,6 +23,7 @@ if ($(allDeviceSelector.join(",")).length) { return; } + // Create all the elements els = $.map(BREAKPOINTS, function(breakpoint) { return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; }); @@ -40,6 +41,7 @@ BreakpointInstance.prototype.getBreakpointSize = function() { var $visibleDevice; $visibleDevice = this.visibleDevice; + // the page refreshed via turbolinks if (!$visibleDevice().length) { this.setup(); } diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 4d066f13646..10abeb50f4b 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -16,6 +16,7 @@ this.toggleSidebar = bind(this.toggleSidebar, this); this.updateDropdown = bind(this.updateDropdown, this); clearInterval(Build.interval); + // Init breakpoint checker this.bp = Breakpoints.get(); $('.js-build-sidebar').niceScroll(); @@ -42,6 +43,9 @@ $(this).data("state", "enabled"); return $(this).text("disable autoscroll"); } + // + // Bind autoscroll button to follow build output + // }); Build.interval = setInterval((function(_this) { return function() { @@ -49,6 +53,10 @@ return _this.getBuildTrace(); } }; + // + // Check for new build output if user still watching build page + // Only valid for runnig build when output changes during time + // })(this), 4000); } } diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js index c0d0b2d049f..e893491b19b 100644 --- a/app/assets/javascripts/commit/image-file.js +++ b/app/assets/javascripts/commit/image-file.js @@ -2,6 +2,7 @@ this.ImageFile = (function() { var prepareFrames; + // Width where images must fits in, for 2-up this gets divided by 2 ImageFile.availWidth = 900; ImageFile.viewModes = ['two-up', 'swipe']; @@ -9,6 +10,7 @@ function ImageFile(file) { this.file = file; this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { + // Determine if old and new file has same dimensions, if not show 'two-up' view return function(deletedWidth, deletedHeight) { return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { if (width === deletedWidth && height === deletedHeight) { diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 37f168c5190..9132089adcd 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -45,6 +45,7 @@ CommitsList.content.html(data.html); return history.replaceState({ page: commitsUrl + // Change url so if user reload a page - search results are saved }, document.title, commitsUrl); }, dataType: "json" diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index c43af17442b..3e20db7e308 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -6,14 +6,19 @@ genericSuccess = function(e) { showTooltip(e.trigger, 'Copied!'); + // Clear the selection and blur the trigger so it loses its border e.clearSelection(); return $(e.trigger).blur(); }; + // Safari doesn't support `execCommand`, so instead we inform the user to + // copy manually. + // + // See http://clipboardjs.com/#browser-support genericError = function(e) { var key; if (/Mac/i.test(navigator.userAgent)) { - key = '⌘'; + key = '⌘'; // Command } else { key = 'Ctrl'; } diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 3dd7ceba92f..c8634b78f2b 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -39,6 +39,9 @@ bottom: unfoldBottom, offset: offset, unfold: unfold, + // indent is used to compensate for single space indent to fit + // '+' and '-' prepended to diff lines, + // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 indent: 1, view: file.data('view') }; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 179d3bc38a5..99b16f7d59b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -164,6 +164,8 @@ } break; case 'projects:network:show': + // Ensure we don't create a particular shortcut handler here. This is + // already created, where the network graph is created. shortcut_handler = true; break; case 'projects:forks:new': @@ -260,12 +262,14 @@ shortcut_handler = new ShortcutsNavigation(); } } + // If we haven't installed a custom shortcut handler, install the default one if (!shortcut_handler) { return new Shortcuts(); } }; Dispatcher.prototype.initSearch = function() { + // Only when search form is present if ($('.search').length) { return new SearchAutocomplete(); } diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 5a725a41fd1..bf68b7e3a9b 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -2,6 +2,7 @@ this.DueDateSelect = (function() { function DueDateSelect() { var $datePicker, $dueDate, $loading; + // Milestone edit/new form $datePicker = $('.datepicker'); if ($datePicker.length) { $dueDate = $('#milestone_due_date'); @@ -16,6 +17,7 @@ e.preventDefault(); return $.datepicker._clearDate($datePicker); }); + // Issuable sidebar $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); $('.js-due-date-select').each(function(i, dropdown) { var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL; @@ -38,6 +40,7 @@ }); addDueDate = function(isDropdown) { var data, date, mediumDate, value; + // Create the post date value = $("input[name='" + fieldName + "']").val(); if (value !== '') { date = new Date(value.replace(new RegExp('-', 'g'), ',')); diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js index ae3dde63da3..4978e24949c 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -1,3 +1,4 @@ +// Disable an element and add the 'disabled' Bootstrap class (function() { $.fn.extend({ disable: function() { @@ -5,6 +6,7 @@ } }); + // Enable an element and remove the 'disabled' Bootstrap class $.fn.extend({ enable: function() { return $(this).removeAttr('disabled').removeClass('disabled'); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 3dca06d36b1..d0786bf0053 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,3 +1,4 @@ +// Creates the variables for setting up GFM auto-completion (function() { if (window.GitLab == null) { window.GitLab = {}; @@ -8,18 +9,22 @@ dataLoaded: false, cachedData: {}, dataSource: '', + // Emoji Emoji: { template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' }, + // Team Members Members: { template: '<li>${username} <small>${title}</small></li>' }, Labels: { template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' }, + // Issues and MergeRequests Issues: { template: '<li><small>${id}</small> ${title}</li>' }, + // Milestones Milestones: { template: '<li>${title}</li>' }, @@ -48,8 +53,11 @@ } }, setup: function(input) { + // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); + // destroy previous instances this.destroyAtWho(); + // set up instances this.setupAtWho(); if (this.dataSource) { if (!this.dataLoading && !this.cachedData) { @@ -63,6 +71,11 @@ return _this.loadData(data); }); }; + // We should wait until initializations are done + // and only trigger the last .setup since + // The previous .dataSource belongs to the previous issuable + // and the last one will have the **proper** .dataSource property + // TODO: Make this a singleton and turn off events when moving to another page })(this), 1000); } if (this.cachedData != null) { @@ -71,6 +84,7 @@ } }, setupAtWho: function() { + // Emoji this.input.atwho({ at: ':', displayTpl: (function(_this) { @@ -90,6 +104,7 @@ beforeInsert: this.DefaultOptions.beforeInsert } }); + // Team Members this.input.atwho({ at: '@', displayTpl: (function(_this) { @@ -321,13 +336,22 @@ loadData: function(data) { this.cachedData = data; this.dataLoaded = true; + // load members this.input.atwho('load', '@', data.members); + // load issues this.input.atwho('load', 'issues', data.issues); + // load milestones this.input.atwho('load', 'milestones', data.milestones); + // load merge requests this.input.atwho('load', 'mergerequests', data.mergerequests); + // load emojis this.input.atwho('load', ':', data.emojis); + // load labels this.input.atwho('load', '~', data.labels); + // load commands this.input.atwho('load', '/', data.commands); + // This trigger at.js again + // otherwise we would be stuck with loading until the user types return $(':focus').trigger('keyup'); } }; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 77b2082cba0..bea141bae51 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -21,12 +21,14 @@ $clearButton = $inputContainer.find('.js-dropdown-input-clear'); this.indeterminateIds = []; $clearButton.on('click', (function(_this) { + // Clear click return function(e) { e.preventDefault(); e.stopPropagation(); return _this.input.val('').trigger('keyup').focus(); }; })(this)); + // Key events timeout = ""; this.input .on('keydown', function (e) { @@ -49,6 +51,7 @@ if (keyCode === 13 && !options.elIsInput) { return false; } + // Only filter asynchronously only if option remote is set if (this.options.remote) { clearTimeout(timeout); return timeout = setTimeout(function() { @@ -79,11 +82,27 @@ if ((data != null) && !this.options.filterByText) { results = data; if (search_text !== '') { + // When data is an array of objects therefore [object Array] e.g. + // [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ] if (_.isArray(data)) { results = fuzzaldrinPlus.filter(data, search_text, { key: this.options.keys }); } else { + // If data is grouped therefore an [object Object]. e.g. + // { + // groupName1: [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ], + // groupName2: [ + // { prop: 'abc' }, + // { prop: 'def' } + // ] + // } if (gl.utils.isObject(data)) { results = {}; for (key in data) { @@ -140,6 +159,7 @@ this.options.beforeSend(); } return this.dataEndpoint("", (function(_this) { + // Fetch the data by calling the data funcfion return function(data) { if (_this.options.success) { _this.options.success(data); @@ -171,6 +191,7 @@ }; })(this) }); + // Fetch the data through ajax if the data is a string }; return GitLabDropdownRemote; @@ -209,13 +230,18 @@ self = this; selector = $(this.el).data("target"); this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + // Set Defaults ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true; + // If no input is passed create a default one self = this; + // If selector was passed if (_.isString(this.filterInput)) { this.filterInput = this.getElement(this.filterInput); } searchFields = this.options.search ? this.options.search.fields : []; if (this.options.data) { + // If we provided data + // data could be an array of objects or a group of arrays if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { this.fullData = this.options.data; currentIndex = -1; @@ -232,10 +258,12 @@ return _this.filter.input.trigger('keyup'); } }; + // Remote data })(this) }); } } + // Init filterable if (this.options.filterable) { this.filter = new GitLabDropdownFilter(this.filterInput, { elIsInput: $(this.el).is('input'), @@ -278,12 +306,14 @@ })(this) }); } + // Event listeners this.dropdown.on("shown.bs.dropdown", this.opened); this.dropdown.on("hidden.bs.dropdown", this.hidden); $(this.el).on("update.label", this.updateLabel); this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); this.dropdown.on('keyup', (function(_this) { return function(e) { + // Escape key if (e.which === 27) { return $('.dropdown-menu-close', _this.dropdown).trigger('click'); } @@ -327,6 +357,7 @@ } } + // Finds an element inside wrapper element GitLabDropdown.prototype.getElement = function(selector) { return this.dropdown.find(selector); }; @@ -344,6 +375,7 @@ } } menu.toggleClass(PAGE_TWO_CLASS); + // Focus first visible input on active page return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); }; @@ -351,23 +383,28 @@ var full_html, groupData, html, name; this.renderedData = data; if (this.options.filterable && data.length === 0) { + // render no matching results html = [this.noResults()]; } else { + // Handle array groups if (gl.utils.isObject(data)) { html = []; for (name in data) { groupData = data[name]; html.push(this.renderItem({ header: name + // Add header for each group }, name)); this.renderData(groupData, name).map(function(item) { return html.push(item); }); } } else { + // Render each row html = this.renderData(data); } } + // Render the full menu full_html = this.renderMenu(html); return this.appendMenu(full_html); }; @@ -406,6 +443,7 @@ if (this.options.setActiveIds) { this.options.setActiveIds.call(this); } + // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); } @@ -427,6 +465,8 @@ if (this.options.filterable) { $input.blur().val(""); } + // Triggering 'keyup' will re-render the dropdown which is not always required + // specially if we want to keep the state of the dropdown needed for bulk-assignment if (!this.options.persistWhenHide) { $input.trigger("keyup"); } @@ -439,6 +479,7 @@ return this.dropdown.trigger('hidden.gl.dropdown'); }; + // Render the full menu GitLabDropdown.prototype.renderMenu = function(html) { var menu_html; menu_html = ""; @@ -450,6 +491,7 @@ return menu_html; }; + // Append the menu into the dropdown GitLabDropdown.prototype.appendMenu = function(html) { var selector; selector = '.dropdown-content'; @@ -465,19 +507,24 @@ group = false; } if (index == null) { + // Render the row index = false; } html = ""; + // Divider if (data === "divider") { return "<li class='divider'></li>"; } + // Separator is a full-width divider if (data === "separator") { return "<li class='separator'></li>"; } + // Header if (data.header != null) { return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header }); } if (this.options.renderRow) { + // Call the render function html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { @@ -489,11 +536,13 @@ selected = true; } } + // Set URL if (this.options.url != null) { url = this.options.url(data); } else { url = data.url != null ? data.url : '#'; } + // Set Text if (this.options.text != null) { text = this.options.text(data); } else { @@ -584,6 +633,7 @@ if (value == null) { field.remove(); } + // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); if (value != null) { if (!field.length && fieldName) { @@ -604,6 +654,7 @@ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { var $input; + // Create hidden input for form $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); if (this.options.inputId != null) { $input.attr('id', this.options.inputId); @@ -625,6 +676,7 @@ if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one " + selector; } + // simulate a click on the first link $el = $(selector, this.dropdown); if ($el.length) { var href = $el.attr('href'); @@ -653,11 +705,15 @@ e.stopImmediatePropagation(); PREV_INDEX = currentIndex; $listItems = $(selector, _this.dropdown); + // if @options.filterable + // $input.blur() if (currentKeyCode === 40) { + // Move down if (currentIndex < ($listItems.length - 1)) { currentIndex += 1; } } else if (currentKeyCode === 38) { + // Move up if (currentIndex > 0) { currentIndex -= 1; } @@ -685,24 +741,32 @@ GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + // Remove the class for the previously focused row $('.is-focused', this.dropdown).removeClass('is-focused'); + // Update the class for the row at the specific index $listItem = $listItems.eq(index); $listItem.find('a:first-child').addClass("is-focused"); + // Dropdown content scroll area $dropdownContent = $listItem.closest('.dropdown-content'); dropdownScrollTop = $dropdownContent.scrollTop(); dropdownContentHeight = $dropdownContent.outerHeight(); dropdownContentTop = $dropdownContent.prop('offsetTop'); dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + // Get the offset bottom of the list item listItemHeight = $listItem.outerHeight(); listItemTop = $listItem.prop('offsetTop'); listItemBottom = listItemTop + listItemHeight; if (!index) { + // Scroll the dropdown content to the top $dropdownContent.scrollTop(0) } else if (index === ($listItems.length - 1)) { + // Scroll the dropdown content to the bottom $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + // Scroll the dropdown content down $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + // Scroll the dropdown content up return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); } }; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 528a673eb15..2703adc0705 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,12 +3,15 @@ function GLForm(form) { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form this.destroy(); + // Setup the form this.setupForm(); this.form.data('gl-form', this); } GLForm.prototype.destroy = function() { + // Clean form listeners this.clearEventListeners(); return this.form.data('gl-form', null); }; @@ -21,12 +24,15 @@ this.form.find('.div-dropzone').remove(); this.form.addClass('gfm-form'); disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + // remove notify commit author checkbox for non-commit notes GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); + // form and textarea event listeners this.addEventListeners(); gl.text.init(this.form); } + // hide discard button this.form.find('.js-note-discard').hide(); return this.form.show(); }; diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index b95faadc8e7..4886da9f21f 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,7 +1,11 @@ - +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// /*= require_tree . */ (function() { - }).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index a646ca1d84f..7d9d4d7c679 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -204,6 +204,7 @@ function ContributorsAuthorGraph(data1) { this.data = data1; + // Don't split graph size in half for mobile devices. if ($(window).width() < 768) { this.width = $('.content').width() - 80; } else { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index fd5b6dc0ddd..7c2eebcdd44 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -38,6 +38,7 @@ return _this.formatSelection.apply(_this, args); }, dropdownCssClass: "ajax-groups-dropdown", + // we do not want to escape markup since we are displaying html in results escapeMarkup: function(m) { return m; } diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 82c14ef0157..53faaa38a0c 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -38,9 +38,11 @@ return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { var $button; $button = $(this); + // Remove the label input box $('input[name="label_name[]"]').filter(function() { return this.value === $button.data('label'); }).remove(); + // Submit the form to get new data Issuable.filterResults($('.filter-form')); return $('.js-label-select').trigger('update.label'); }); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index e6422602ce8..261bf6137c2 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,10 +1,6 @@ /*= require flash */ - - /*= require jquery.waitforimages */ - - /*= require task_list */ (function() { @@ -13,6 +9,7 @@ this.Issue = (function() { function Issue() { this.submitNoteForm = bind(this.submitNoteForm, this); + // Prevent duplicate event bindings this.disableTaskList(); if ($('a.btn-close').length) { this.initTaskList(); @@ -99,6 +96,8 @@ url: $('form.js-issuable-update').attr('action'), data: patchData }); + // TODO (rspeicher): Make the issue description inline-editable like a note so + // that we can re-use its form here }; Issue.prototype.initMergeRequests = function() { @@ -128,6 +127,8 @@ Issue.prototype.initCanCreateBranch = function() { var $container; $container = $('#new-branch'); + // If the user doesn't have the required permissions the container isn't + // rendered at all. if ($container.length === 0) { return; } diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js index 8ca90490426..62a7fc9a06c 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js +++ b/app/assets/javascripts/issues-bulk-assignment.js @@ -1,14 +1,17 @@ (function() { this.IssuableBulkActions = (function() { function IssuableBulkActions(opts) { + // Set defaults var ref, ref1, ref2; if (opts == null) { opts = {}; } this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li'); + // Save instance this.form.data('bulkActions', this); this.willUpdateLabels = false; this.bindEvents(); + // Fixes bulk-assign not working when navigating through pages Issuable.initChecks(); } @@ -86,6 +89,7 @@ ref1 = this.getLabelsFromSelection(); for (j = 0, len1 = ref1.length; j < len1; j++) { id = ref1[j]; + // Only the ones that we are not going to keep if (labelsToKeep.indexOf(id) === -1) { result.push(id); } @@ -147,6 +151,8 @@ indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); labelsToApply = this.getLabelsToApply(); indeterminatedLabels.map(function(id) { + // We need to exclude label IDs that will be applied + // By not doing this will cause issues from selection to not add labels at all if (labelsToApply.indexOf(id) === -1) { return result.push(id); } diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index fe071fca67c..cb16e2ba814 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -26,13 +26,16 @@ var previewColor; previewColor = $('input#label_color').val(); return $('div.label-color-preview').css('background-color', previewColor); + // Updates the the preview color with the hex-color input }; + // Updates the preview color with a click on a suggested color Labels.prototype.setSuggestedColor = function(e) { var color; color = $(e.currentTarget).data('color'); $('input#label_color').val(color); this.updateColorPreview(); + // Notify the form, that color has changed $('.label-form').trigger('keyup'); return e.preventDefault(); }; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index bab23ff5ac0..29a967a35a0 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -156,11 +156,13 @@ selectedClass.push('is-indeterminate'); } if (active.indexOf(label.id) !== -1) { + // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); + // Add input manually instance.addInput(this.fieldName, label.id); } } @@ -172,6 +174,7 @@ } if (label.duplicate) { spacing = 100 / label.color.length; + // Reduce the colors to 4 label.color = label.color.filter(function(color, i) { return i < 4; }); @@ -192,11 +195,13 @@ } else { colorEl = ''; } + // We need to identify which items are actually labels if (label.id) { selectedClass.push('label-item'); $a.attr('data-label-id', label.id); } $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + // Return generated html return $li.html($a).prop('outerHTML'); }, persistWhenHide: $dropdown.data('persistWhenHide'), @@ -238,6 +243,7 @@ isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; $selectbox.hide(); + // display:block overrides the hide-collapse rule $value.removeAttr('style'); if (page === 'projects:boards:show') { return; @@ -255,6 +261,7 @@ } } if ($dropdown.hasClass('js-filter-bulk-update')) { + // If we are persisting state we need the classes if (!this.options.persistWhenHide) { return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); } @@ -324,7 +331,9 @@ if ($('.selected_issue:checked').length) { return; } + // Remove inputs $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); + // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js index 8d5e52286b7..d9b07c10a49 100644 --- a/app/assets/javascripts/lib/chart.js +++ b/app/assets/javascripts/lib/chart.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js index 8ee81804513..a88e640f298 100644 --- a/app/assets/javascripts/lib/cropper.js +++ b/app/assets/javascripts/lib/cropper.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js index 31e6033e756..ee1baf54803 100644 --- a/app/assets/javascripts/lib/d3.js +++ b/app/assets/javascripts/lib/d3.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js index 923c575dcfe..6df427bc2b1 100644 --- a/app/assets/javascripts/lib/raphael.js +++ b/app/assets/javascripts/lib/raphael.js @@ -1,13 +1,8 @@ /*= require raphael */ - - /*= require g.raphael */ - - /*= require g.bar */ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d4d5927d3b0..8fdf4646cd8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -29,6 +29,7 @@ if (setTimeago) { $timeagoEls.timeago(); $timeagoEls.tooltip('destroy'); + // Recreate with custom template return $timeagoEls.tooltip({ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' }); diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb deleted file mode 100644 index 80f9936b9c2..00000000000 --- a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb +++ /dev/null @@ -1,2 +0,0 @@ -gl.emojiAliases = -> - JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb new file mode 100644 index 00000000000..aeb86c9fa5b --- /dev/null +++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb @@ -0,0 +1,6 @@ +(function() { + gl.emojiAliases = function() { + return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>'); + }; + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 42b6ac0589e..5b338b00d76 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -6,6 +6,7 @@ notification = new Notification(message, opts); setTimeout(function() { return notification.close(); + // Hide the notification after X amount of seconds }, 8000); if (onclick) { return notification.onclick = onclick; @@ -22,12 +23,16 @@ body: body, icon: icon }; + // Let's check if the browser supports notifications if (!('Notification' in window)) { + // do nothing } else if (Notification.permission === 'granted') { + // If it's okay let's create a notification return notificationGranted(message, opts, onclick); } else if (Notification.permission !== 'denied') { return Notification.requestPermission(function(permission) { + // If the user accepts, let's create a notification if (permission === 'granted') { return notificationGranted(message, opts, onclick); } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index b6636de5767..d761a844be9 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -29,6 +29,7 @@ lineBefore = this.lineBefore(text, textArea); lineAfter = this.lineAfter(text, textArea); if (lineBefore === blockTag && lineAfter === blockTag) { + // To remove the block tag we have to select the line before & after if (blockTag != null) { textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); @@ -63,11 +64,11 @@ if (!inserted) { try { document.execCommand("ms-beginUndoUnit"); - } catch (undefined) {} + } catch (error) {} textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); try { document.execCommand("ms-endUndoUnit"); - } catch (undefined) {} + } catch (error) {} } return this.moveCursor(textArea, tag, wrap); }; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 533310cc87c..f84a20cf0fe 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -7,6 +7,8 @@ if ((base = w.gl).utils == null) { base.utils = {}; } + // Returns an array containing the value(s) of the + // of the key passed as an argument w.gl.utils.getParameterValues = function(sParam) { var i, sPageURL, sParameterName, sURLVariables, values; sPageURL = decodeURIComponent(window.location.search.substring(1)); @@ -23,6 +25,8 @@ } return values; }; + // @param {Object} params - url keys and value to merge + // @param {String} url w.gl.utils.mergeUrlParams = function(params, url) { var lastChar, newUrl, paramName, paramValue, pattern; newUrl = decodeURIComponent(url); @@ -37,12 +41,14 @@ newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; } } + // Remove a trailing ampersand lastChar = newUrl[newUrl.length - 1]; if (lastChar === '&') { newUrl = newUrl.slice(0, -1); } return newUrl; }; + // removes parameter query string from url. returns the modified url w.gl.utils.removeParamQueryString = function(url, param) { var urlVariables, variables; url = decodeURIComponent(url); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index f145bd3ad74..93daea1dce7 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,17 +1,49 @@ - +// LineHighlighter +// +// Handles single- and multi-line selection and highlight for blob views. +// /*= require jquery.scrollTo */ +// +// ### Example Markup +// +// <div id="blob-content-holder"> +// <div class="file-content"> +// <div class="line-numbers"> +// <a href="#L1" id="L1" data-line-number="1">1</a> +// <a href="#L2" id="L2" data-line-number="2">2</a> +// <a href="#L3" id="L3" data-line-number="3">3</a> +// <a href="#L4" id="L4" data-line-number="4">4</a> +// <a href="#L5" id="L5" data-line-number="5">5</a> +// </div> +// <pre class="code highlight"> +// <code> +// <span id="LC1" class="line">...</span> +// <span id="LC2" class="line">...</span> +// <span id="LC3" class="line">...</span> +// <span id="LC4" class="line">...</span> +// <span id="LC5" class="line">...</span> +// </code> +// </pre> +// </div> +// </div> +// (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; this.LineHighlighter = (function() { + // CSS class applied to highlighted lines LineHighlighter.prototype.highlightClass = 'hll'; + // Internal copy of location.hash so we're not dependent on `location` in tests LineHighlighter.prototype._hash = ''; function LineHighlighter(hash) { var range; if (hash == null) { + // Initialize a LineHighlighter object + // + // hash - String URL hash for dependency injection in tests hash = location.hash; } this.setHash = bind(this.setHash, this); @@ -24,6 +56,8 @@ if (range[0]) { this.highlightRange(range); $.scrollTo("#L" + range[0], { + // Scroll to the first highlighted line on initial load + // Offset -50 for the sticky top bar, and another -100 for some context offset: -150 }); } @@ -32,6 +66,12 @@ LineHighlighter.prototype.bindEvents = function() { $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler); + // While it may seem odd to bind to the mousedown event and then throw away + // the click event, there is a method to our madness. + // + // If not done this way, the line number anchor will sometimes keep its + // active state even when the event is cancelled, resulting in an ugly border + // around the link and/or a persisted underline text decoration. return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { return event.preventDefault(); }); @@ -44,6 +84,8 @@ lineNumber = $(event.target).closest('a').data('line-number'); current = this.hashToRange(this._hash); if (!(current[0] && event.shiftKey)) { + // If there's no current selection, or there is but Shift wasn't held, + // treat this like a single-line selection. this.setHash(lineNumber); return this.highlightLine(lineNumber); } else if (event.shiftKey) { @@ -59,10 +101,23 @@ LineHighlighter.prototype.clearHighlight = function() { return $("." + this.highlightClass).removeClass(this.highlightClass); + // Unhighlight previously highlighted lines }; + // Convert a URL hash String into line numbers + // + // hash - Hash String + // + // Examples: + // + // hashToRange('#L5') # => [5, null] + // hashToRange('#L5-15') # => [5, 15] + // hashToRange('#foo') # => [null, null] + // + // Returns an Array LineHighlighter.prototype.hashToRange = function(hash) { var first, last, matches; + //?L(\d+)(?:-(\d+))?$/) matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); if (matches && matches.length) { first = parseInt(matches[1]); @@ -73,10 +128,16 @@ } }; + // Highlight a single line + // + // lineNumber - Line number to highlight LineHighlighter.prototype.highlightLine = function(lineNumber) { return $("#LC" + lineNumber).addClass(this.highlightClass); }; + // Highlight all lines within a range + // + // range - Array containing the starting and ending line numbers LineHighlighter.prototype.highlightRange = function(range) { var i, lineNumber, ref, ref1, results; if (range[1]) { @@ -90,6 +151,7 @@ } }; + // Set the URL hash string LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { var hash; if (lastLineNumber) { @@ -101,10 +163,15 @@ return this.__setLocationHash__(hash); }; + // Make the actual hash change in the browser + // + // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { return history.pushState({ turbolinks: false, url: value + // We're using pushState instead of assigning location.hash directly to + // prevent the page from scrolling on the hashchange event }, document.title, value); }; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 56ebf84c4f6..05644b3d03c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,10 +1,6 @@ /*= require jquery.waitforimages */ - - /*= require task_list */ - - /*= require merge_request_tabs */ (function() { @@ -12,6 +8,11 @@ this.MergeRequest = (function() { function MergeRequest(opts) { + // Initialize MergeRequest behavior + // + // Options: + // action - String, current controller action + // this.opts = opts != null ? opts : {}; this.submitNoteForm = bind(this.submitNoteForm, this); this.$el = $('.merge-request'); @@ -21,6 +22,7 @@ }; })(this)); this.initTabs(); + // Prevent duplicate event bindings this.disableTaskList(); this.initMRBtnListeners(); if ($("a.btn-close").length) { @@ -28,14 +30,17 @@ } } + // Local jQuery finder MergeRequest.prototype.$ = function(selector) { return this.$el.find(selector); }; MergeRequest.prototype.initTabs = function() { if (this.opts.action !== 'new') { + // `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs = new MergeRequestTabs(this.opts); } else { + // Show the first tab (Commits) return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); } }; @@ -96,6 +101,8 @@ url: $('form.js-issuable-update').attr('action'), data: patchData }); + // TODO (rspeicher): Make the merge request description inline-editable like a + // note so that we can re-use its form here }; return MergeRequest; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index ad08209d61e..5316101457c 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,6 +1,49 @@ - +// MergeRequestTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the MergeRequests#show page. +// /*= require jquery.cookie */ +// +// ### Example Markup +// +// <ul class="nav-links merge-request-tabs"> +// <li class="notes-tab active"> +// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +// Discussion +// </a> +// </li> +// <li class="commits-tab"> +// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +// Commits +// </a> +// </li> +// <li class="diffs-tab"> +// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +// Diffs +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="notes tab-pane active" id="notes"> +// Notes Content +// </div> +// <div class="commits tab-pane" id="commits"> +// Commits Content +// </div> +// <div class="diffs tab-pane" id="diffs"> +// Diffs Content +// </div> +// </div> +// +// <div class="mr-loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -19,6 +62,7 @@ this.setCurrentAction = bind(this.setCurrentAction, this); this.tabShown = bind(this.tabShown, this); this.showTab = bind(this.showTab, this); + // Store the `location` object, allowing for easier stubbing in tests this._location = location; this.bindEvents(); this.activateTab(this.opts.action); @@ -77,6 +121,7 @@ } }; + // Activate a tab based on the current action MergeRequestTabs.prototype.activateTab = function(action) { if (action === 'show') { action = 'notes'; @@ -84,20 +129,48 @@ return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); }; + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('notes') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String MergeRequestTabs.prototype.setCurrentAction = function(action) { var new_state; + // Normalize action, just to be safe if (action === 'show') { action = 'notes'; } this.currentAction = action; + // Remove a trailing '/commits' or '/diffs' new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); + // Append the new action if we're on a tab other than 'notes' if (action !== 'notes') { new_state += "/" + action; } + // Ensure parameters and hash come along for the ride new_state += this._location.search + this._location.hash; history.replaceState({ turbolinks: true, url: new_state + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 }, document.title, new_state); return new_state; }; @@ -191,6 +264,7 @@ }); }; +<<<<<<< a79ff9346b73079148cc4ecc81da82804bb51f3c MergeRequestTabs.prototype.loadPipelines = function(source) { if (this.pipelinesLoaded) { return; @@ -206,6 +280,11 @@ }); }; +======= + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide +>>>>>>> Restore comments lost when converting CoffeeScript to JavaScript MergeRequestTabs.prototype.toggleLoading = function(status) { return $('.mr-loading-status .loading').toggle(status); }; @@ -232,6 +311,7 @@ MergeRequestTabs.prototype.diffViewType = function() { return $('.inline-parallel-buttons a.active').data('view-type'); + // Returns diff view type }; MergeRequestTabs.prototype.expandViewContainer = function() { @@ -245,6 +325,8 @@ if ($gutterIcon.is('.fa-angle-double-right')) { return $gutterIcon.closest('a').trigger('click', [true]); } + // Wait until listeners are set + // Only when sidebar is expanded }, 0); }; @@ -259,6 +341,9 @@ return $gutterIcon.closest('a').trigger('click', [true]); } }, 0); + // Expand the issuable sidebar unless the user explicitly collapsed it + // Wait until listeners are set + // Only when sidebar is collapsed }; return MergeRequestTabs; diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index bd35b6f679d..7bbcdf59838 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -3,6 +3,12 @@ this.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { + // Initialize MergeRequestWidget behavior + // + // check_enable - Boolean, whether to check automerge status + // merge_check_url - String, URL to use to check automerge status + // ci_status_url - String, URL to use to check CI status + // this.opts = opts; $('#modal_merge_info').modal({ show: false @@ -118,6 +124,8 @@ if (data.coverage) { _this.showCICoverage(data.coverage); } + // The first check should only update the UI, a notification + // should only be displayed on status changes if (showNotification && !_this.firstCICheck) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index e8d51da7d58..bc1a99057d9 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -110,6 +110,7 @@ }, update: function(event, ui) { var data; + // Prevents sorting from container which element has been removed. if ($(this).find(ui.item).length > 0) { data = $(this).sortable("serialize"); return Milestone.sortIssues(data); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index e897ebdf630..c8031174dd2 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -92,6 +92,7 @@ }, hidden: function() { $selectbox.hide(); + // display:block overrides the hide-collapse rule return $value.css('display', ''); }, clicked: function(selected, $el, e) { diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js index c0fec1f8607..91132af273a 100644 --- a/app/assets/javascripts/network/branch-graph.js +++ b/app/assets/javascripts/network/branch-graph.js @@ -90,6 +90,7 @@ results = []; while (k < this.mspace) { this.colors.push(Raphael.getColor(.8)); + // Skipping a few colors in the spectrum to get more contrast between colors Raphael.getColor(); Raphael.getColor(); results.push(k++); @@ -112,6 +113,7 @@ for (mm = j = 0, len = ref.length; j < len; mm = ++j) { day = ref[mm]; if (cuday !== day[0] || cumonth !== day[1]) { + // Dates r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ font: "12px Monaco, monospace", fill: "#BBB" @@ -119,6 +121,7 @@ cuday = day[0]; } if (cumonth !== day[1]) { + // Months r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ font: "12px Monaco, monospace", fill: "#EEE" @@ -207,6 +210,7 @@ } r = this.r; shortrefs = commit.refs; + // Truncate if longer than 15 chars if (shortrefs.length > 17) { shortrefs = shortrefs.substr(0, 15) + "…"; } @@ -217,6 +221,7 @@ title: commit.refs }); textbox = text.getBBox(); + // Create rectangle based on the size of the textbox rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ fill: "#000", "fill-opacity": .5, @@ -229,6 +234,7 @@ }); label = r.set(rect, text); label.transform(["t", -rect.getBBox().width - 15, 0]); + // Set text to front return text.toFront(); }; @@ -283,11 +289,13 @@ parentY = this.offsetY + this.unitTime * parentCommit.time; parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); + // Set line color if (parentCommit.space <= commit.space) { color = this.colors[commit.space]; } else { color = this.colors[parentCommit.space]; } + // Build line shape if (parent[1] === commit.space) { offset = [0, 5]; arrow = "l-2,5,4,0,-2,-5,0,5"; @@ -298,13 +306,17 @@ offset = [-3, 3]; arrow = "l-5,0,2,4,3,-4,-4,2"; } + // Start point route = ["M", x + offset[0], y + offset[1]]; + // Add arrow if not first parent if (i > 0) { route.push(arrow); } + // Circumvent if overlap if (commit.space !== parentCommit.space || commit.space !== parent[1]) { route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); } + // End point route.push("L", parentX1, parentY); results.push(r.path(route).attr({ stroke: color, @@ -325,6 +337,7 @@ "fill-opacity": .5, stroke: "none" }); + // Displayed in the center return this.element.scrollTop(y - this.graphHeight / 2); } }; diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 6a7422a7755..67c3e645364 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,9 @@ - +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index ebe9ff2b8c4..c6854f703fb 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,22 +1,10 @@ /*= require autosave */ - - /*= require autosize */ - - /*= require dropzone */ - - /*= require dropzone_input */ - - /*= require gfm_auto_complete */ - - /*= require jquery.atwho */ - - /*= require task_list */ (function() { @@ -60,26 +48,43 @@ } Notes.prototype.addBinding = function() { + // add note to UI after creation $(document).on("ajax:success", ".js-main-target-form", this.addNote); $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote); + // catch note ajax errors $(document).on("ajax:error", ".js-main-target-form", this.addNoteError); + // change note in UI after update $(document).on("ajax:success", "form.edit-note", this.updateNote); + // Edit note link $(document).on("click", ".js-note-edit", this.showEditForm); $(document).on("click", ".note-edit-cancel", this.cancelEdit); + // Reopen and close actions for Issue/MR combined with note form submit $(document).on("click", ".js-comment-button", this.updateCloseButton); $(document).on("keyup input", ".js-note-text", this.updateTargetButtons); + // resolve a discussion $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion); + // remove a note (in general) $(document).on("click", ".js-note-delete", this.removeNote); + // delete note attachment $(document).on("click", ".js-note-attachment-delete", this.removeAttachment); + // reset main target form after submit $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton); $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm); + // reset main target form when clicking discard $(document).on("click", ".js-note-discard", this.resetMainTargetForm); + // update the file name when an attachment is selected $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); + // reply to diff/discussion notes $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote); + // add diff note $(document).on("click", ".js-add-diff-note-button", this.addDiffNote); + // hide diff note form $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); + // fetch notes when tab becomes visible $(document).on("visibilitychange", this.visibilityChange); + // when issue status changes, we need to refresh data $(document).on("issuable:change", this.refresh); + // when a key is clicked on the notes return $(document).on("keydown", ".js-note-text", this.keydownNoteText); }; @@ -112,6 +117,7 @@ return; } $textarea = $(e.target); + // Edit previous note when UP arrow is hit switch (e.which) { case 38: if ($textarea.val() !== '') { @@ -123,6 +129,7 @@ return myLastNoteEditBtn.trigger('click', [true, myLastNote]); } break; + // Cancel creating diff note or editing any note when ESCAPE is hit case 27: discussionNoteForm = $textarea.closest('.js-discussion-note-form'); if (discussionNoteForm.length) { @@ -247,10 +254,13 @@ votesBlock = $('.js-awards-block').eq(0); gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name); return gl.awardsHandler.scrollToAwards(); + // render note if it not present in loaded list + // or skip if rendered } else if (this.isNewNote(note)) { this.note_ids.push(note.id); $notesList = $('ul.main-notes-list'); $notesList.append(note.html).syntaxHighlight(); + // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); this.initTaskList(); this.refresh(); @@ -291,19 +301,26 @@ row = form.closest("tr"); note_html = $(note.html); note_html.syntaxHighlight(); + // is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); if ((note.original_discussion_id != null) && discussionContainer.length === 0) { discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); } if (discussionContainer.length === 0) { + // insert the note and the reply button after the temp row row.after(note.diff_discussion_html); + // remove the note (will be added again below) row.next().find(".note").remove(); + // Before that, the container didn't exist discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + // Add note to 'Changes' page discussions discussionContainer.append(note_html); + // Init discussion on 'Discussion' page if it is merge request page if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); } } else { + // append new note to all matching discussions discussionContainer.append(note_html); } @@ -327,7 +344,9 @@ Notes.prototype.resetMainTargetForm = function(e) { var form; form = $(".js-main-target-form"); + // remove validation errors form.find(".js-errors").remove(); + // reset text and preview form.find(".js-md-write-button").click(); form.find(".js-note-text").val("").trigger("input"); form.find(".js-note-text").data("autosave").reset(); @@ -354,9 +373,13 @@ Notes.prototype.setupMainTargetNoteForm = function() { var form; + // find the form form = $(".js-new-note-form"); + // Set a global clone of the form for later cloning this.formClone = form.clone(); + // show the form this.setupNoteForm(form); + // fix classes form.removeClass("js-new-note-form"); form.addClass("js-main-target-form"); form.find("#note_line_code").remove(); @@ -421,6 +444,7 @@ } this.renderDiscussionNote(note); + // cleanup after successfully creating a diff/discussion note this.removeDiscussionNoteForm($form); }; @@ -433,10 +457,12 @@ Notes.prototype.updateNote = function(_xhr, note, _status) { var $html, $note_li; + // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); gl.utils.localTimeAgo($('.js-timeago', $html)); $html.syntaxHighlight(); $html.find('.js-task-list-container').taskList('enable'); + // Find the note's `li` element by ID and replace it with the updated HTML $note_li = $('.note-row-' + note.id); $note_li.replaceWith($html); @@ -461,15 +487,20 @@ note.addClass("is-editting"); form = note.find(".note-edit-form"); form.addClass('current-note-edit-form'); + // Show the attachment delete link note.find(".js-note-attachment-delete").show(); done = function($noteText) { var noteTextVal; + // Neat little trick to put the cursor at the end noteTextVal = $noteText.val(); + // Store the original note text in a data attribute to retrieve if a user cancels edit. form.find('form.edit-note').data('original-note', noteTextVal); return $noteText.val('').val(noteTextVal); }; new GLForm(form); if ((scrollTo != null) && (myLastNote != null)) { + // scroll to the bottom + // so the open of the last element doesn't make a jump $('html, body').scrollTop($(document).height()); return $('html, body').animate({ scrollTop: myLastNote.offset().top - 150 @@ -505,6 +536,7 @@ form = note.find(".current-note-edit-form"); note.removeClass("is-editting"); form.removeClass("current-note-edit-form"); + // Replace markdown textarea text with original note text. return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); }; @@ -520,6 +552,9 @@ var noteId; noteId = $(e.currentTarget).closest(".note").attr("id"); $(".note[id='" + noteId + "']").each((function(_this) { + // A same note appears in the "Discussion" and in the "Changes" tab, we have + // to remove all. Using $(".note[id='noteId']") ensure we get all the notes, + // where $("#noteId") would return only one. return function(i, el) { var note, notes; note = $(el); @@ -533,13 +568,17 @@ } } + // check if this is the last note for this line if (notes.find(".note").length === 1) { + // "Discussions" tab notes.closest(".timeline-entry").remove(); + // "Changes" tab / commit view notes.closest("tr").remove(); } return note.remove(); }; })(this)); + // Decrement the "Discussions" counter only once return this.updateNotesCount(-1); }; @@ -571,10 +610,12 @@ var form, replyLink; form = this.formClone.clone(); replyLink = $(e.target).closest(".js-discussion-reply-button"); + // insert the form after the button replyLink .closest('.discussion-reply-holder') .hide() .after(form); + // show the form return this.setupDiscussionNoteForm(replyLink, form); }; @@ -589,6 +630,7 @@ */ Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { + // setup note target form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId"))); form.attr("data-line-code", dataHolder.data("lineCode")); form.find("#note_type").val(dataHolder.data("noteType")); @@ -636,6 +678,7 @@ addForm = false; notesContentSelector = ".notes_content"; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>"; + // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineType = $link.data("lineType"); notesContentSelector += "." + lineType; @@ -652,6 +695,7 @@ e.target = replyButton[0]; $.proxy(this.replyToDiscussionNote, replyButton[0], e).call(); } else { + // In parallel view, the form may not be present in one of the panes noteForm = notesContent.find(".js-discussion-note-form"); if (noteForm.length === 0) { addForm = true; @@ -659,6 +703,7 @@ } } } else { + // add a notes row and insert the form row.after(rowCssToAdd); nextRow = row.next(); notesContent = nextRow.find(notesContentSelector); @@ -667,6 +712,7 @@ if (addForm) { newForm = this.formClone.clone(); newForm.appendTo(notesContent); + // show the form return this.setupDiscussionNoteForm($link, newForm); } }; @@ -685,12 +731,15 @@ glForm = form.data('gl-form'); glForm.destroy(); form.find(".js-note-text").data("autosave").reset(); + // show the reply button (will only work for replies) form .prev('.discussion-reply-holder') .show(); if (row.is(".js-temp-notes-holder")) { + // remove temporary row for diff lines return row.remove(); } else { + // only remove the form return form.remove(); } }; @@ -712,6 +761,7 @@ Notes.prototype.updateFormAttachment = function() { var filename, form; form = $(this).closest("form"); + // get only the basename filename = $(this).val().replace(/^.*[\\\/]/, ""); return form.find(".js-attachment-filename").text(filename); }; diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 5fd75799640..5200487814f 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,9 +1,15 @@ +// MarkdownPreview +// +// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, +// and showing a warning when more than `x` users are referenced. +// (function() { var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; this.MarkdownPreview = (function() { function MarkdownPreview() {} + // Minimum number of users referenced before triggering a warning MarkdownPreview.prototype.referenceThreshold = 10; MarkdownPreview.prototype.ajaxCache = {}; @@ -101,8 +107,10 @@ return; } lastTextareaPreviewed = $form.find('textarea.markdown-area'); + // toggle tabs $form.find(writeButtonSelector).parent().removeClass('active'); $form.find(previewButtonSelector).parent().addClass('active'); + // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); return markdownPreview.showPreview($form); @@ -113,8 +121,10 @@ return; } lastTextareaPreviewed = null; + // toggle tabs $form.find(writeButtonSelector).parent().addClass('active'); $form.find(previewButtonSelector).parent().removeClass('active'); + // toggle content $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); return $form.find('.md-preview-holder').hide(); diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index a3eea316f67..30cd6f6e470 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -5,6 +5,7 @@ GitLabCrop = (function() { var FILENAMEREGEX; + // Matches everything but the file name FILENAMEREGEX = /^.*[\\\/]/; function GitLabCrop(input, opts) { @@ -17,11 +18,18 @@ this.onModalShow = bind(this.onModalShow, this); this.onPickImageClick = bind(this.onPickImageClick, this); this.fileInput = $(input); + // We should rename to avoid spec to fail + // Form will submit the proper input filed with a file using FormData this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger"); + // Set defaults this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg; + // Required params + // Ensure needed elements are jquery objects + // If selector is provided we will convert them to a jQuery Object this.filename = this.getElement(this.filename); this.previewImage = this.getElement(this.previewImage); this.pickImageEl = this.getElement(this.pickImageEl); + // Modal elements usually are outside the @form element this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop; this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; @@ -93,8 +101,8 @@ return this.modalCropImg.attr('src', '').cropper('destroy'); }; - GitLabCrop.prototype.onUploadImageBtnClick = function(e) { - e.preventDefault(); + GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image + e.preventDefault(); // Destroy cropper instance this.setBlob(); this.setPreview(); this.modalCrop.modal('hide'); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index ed1d87abafe..60f9fba5777 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -11,9 +11,11 @@ this.form = (ref = opts.form) != null ? ref : $('.edit-user'); $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() { return $(this).parents('form').submit(); + // Automatically submit the Preferences form when any of its radio buttons change }); $('#user_notification_email').on('change', function() { return $(this).parents('form').submit(); + // Automatically submit email form when it changes }); $('.update-username').on('ajax:before', function() { $('.loading-username').show(); @@ -76,6 +78,7 @@ }, complete: function() { window.scrollTo(0, 0); + // Enable submit button after requests ends return self.form.find(':input[disabled]').enable(); } }); @@ -93,6 +96,7 @@ if (comment && comment.length > 1 && $title.val() === '') { return $title.val(comment[1]).change(); } + // Extract the SSH Key title from its comment }); if (gl.utils.getPagePath() === 'profiles') { return new Profile(); diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index b95faadc8e7..d6e4d9f7ad8 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 66e097c0a28..a6c015299a0 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -11,7 +11,13 @@ url = $("#project_clone").val(); $('#project_clone').val(url); return $('.clone').text(url); + // Git protocol switcher + // Remove the active class for all buttons (ssh, http, kerberos if shown) + // Add the active class for the clicked button + // Update the input field + // Update the command line instructions }); + // Ref switcher this.initRefSwitcher(); $('.project-refs-select').on('change', function() { return $(this).parents('form').submit(); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 4925f0519f0..5bf900f3e1d 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -13,8 +13,11 @@ this.selectRowUp = bind(this.selectRowUp, this); this.filePaths = {}; this.inputElement = this.element.find(".file-finder-input"); + // init event this.initEvent(); + // focus text input box this.inputElement.focus(); + // load file list this.load(this.options.url); } @@ -42,6 +45,7 @@ } } }); + // init event }; ProjectFindFile.prototype.findFile = function() { @@ -49,8 +53,10 @@ searchText = this.inputElement.val(); result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; return this.renderList(result, searchText); + // find file }; + // files pathes load ProjectFindFile.prototype.load = function(url) { return $.ajax({ url: url, @@ -67,6 +73,7 @@ }); }; + // render result ProjectFindFile.prototype.renderList = function(filePaths, searchText) { var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); @@ -86,6 +93,7 @@ return results; }; + // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) highlighter = function(element, text, matches) { var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; lastIndex = 0; @@ -110,6 +118,7 @@ return element.append(document.createTextNode(text.substring(lastIndex))); }; + // make tbody row html ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { var $tr; $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>"); diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index 8ca4c427912..c8cfc9a9ba8 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -7,3 +7,5 @@ })(); }).call(this); + +// I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 4f415b05dbc..04fb49552e8 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -33,6 +33,7 @@ $('.projects-list-holder').replaceWith(data.html); return history.replaceState({ page: project_filter_url + // Change url so if user reload a page - search results are saved }, document.title, project_filter_url); }, dataType: "json" diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 index 6738dc8862d..983322cbecc 100644 --- a/app/assets/javascripts/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_dropdown.js.es6 @@ -45,6 +45,7 @@ class ProtectedBranchDropdown { } onClickCreateWildcard() { + // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').selectRowAtIndex(0); } diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 227e8c696b4..8abb09c626f 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -24,6 +24,7 @@ this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this); this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this); this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || ''; + // Dropdown Element this.dropdown = this.wrap.find('.dropdown'); this.dropdownContent = this.dropdown.find('.dropdown-content'); this.locationBadgeEl = this.getElement('.location-badge'); @@ -35,6 +36,7 @@ this.repositoryInputEl = this.getElement('#repository_ref'); this.clearInput = this.getElement('.js-clear-input'); this.saveOriginalState(); + // Only when user is logged in if (gon.current_user_id) { this.createAutocomplete(); } @@ -43,6 +45,7 @@ this.bindEvents(); } + // Finds an element inside wrapper element SearchAutocomplete.prototype.getElement = function(selector) { return this.wrap.find(selector); }; @@ -82,6 +85,7 @@ } return; } + // Prevent multiple ajax calls if (this.loadingSuggestions) { return; } @@ -92,14 +96,17 @@ term: term }, function(response) { var data, firstCategory, i, lastCategory, len, suggestion; + // Hide dropdown menu if no suggestions returns if (!response.length) { _this.disableAutocomplete(); return; } data = []; + // List results firstCategory = true; for (i = 0, len = response.length; i < len; i++) { suggestion = response[i]; + // Add group header before list each group if (lastCategory !== suggestion.category) { if (!firstCategory) { data.push('separator'); @@ -119,6 +126,7 @@ url: suggestion.url }); } + // Add option to proceed with the search if (data.length) { data.push('separator'); data.push({ @@ -169,11 +177,13 @@ SearchAutocomplete.prototype.serializeState = function() { return { + // Search Criteria search_project_id: this.projectInputEl.val(), group_id: this.groupInputEl.val(), search_code: this.searchCodeInputEl.val(), repository_ref: this.repositoryInputEl.val(), scope: this.scopeInputEl.val(), + // Location badge _location: this.locationBadgeEl.text() }; }; @@ -194,6 +204,7 @@ SearchAutocomplete.prototype.enableAutocomplete = function() { var _this; + // No need to enable anything if user is not logged in if (!gon.current_user_id) { return; } @@ -206,18 +217,22 @@ }; SearchAutocomplete.prototype.onSearchInputKeyDown = function() { + // Saves last length of the entered text return this.saveTextLength(); }; SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { switch (e.keyCode) { case KEYCODE.BACKSPACE: + // when trying to remove the location badge if (this.lastTextLength === 0 && this.badgePresent()) { this.removeLocationBadge(); } + // When removing the last character and no badge is present if (this.lastTextLength === 1) { this.disableAutocomplete(); } + // When removing any character from existin value if (this.lastTextLength > 1) { this.enableAutocomplete(); } @@ -232,9 +247,12 @@ case KEYCODE.DOWN: return; default: + // Handle the case when deleting the input value other than backspace + // e.g. Pressing ctrl + backspace or ctrl + x if (this.searchInput.val() === '') { this.disableAutocomplete(); } else { + // We should display the menu only when input is not empty if (e.keyCode !== KEYCODE.ENTER) { this.enableAutocomplete(); } @@ -243,7 +261,9 @@ this.wrap.toggleClass('has-value', !!e.target.value); }; + // Avoid falsy value to be returned SearchAutocomplete.prototype.onSearchInputClick = function(e) { + // Prevents closing the dropdown menu return e.stopImmediatePropagation(); }; @@ -267,6 +287,7 @@ SearchAutocomplete.prototype.onSearchInputBlur = function(e) { this.isFocused = false; this.wrap.removeClass('search-active'); + // If input is blank then restore state if (this.searchInput.val() === '') { return this.restoreOriginalState(); } @@ -311,6 +332,7 @@ results = []; for (i = 0, len = inputs.length; i < len; i++) { input = inputs[i]; + // _location isnt a input if (input === '_location') { break; } diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 3b28332854a..3aa8536d40a 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -86,6 +86,7 @@ var defaultStopCallback; defaultStopCallback = Mousetrap.stopCallback; return function(e, element, combo) { + // allowed shortcuts if textarea, input, contenteditable are focused if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { return false; } else { diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 6c78914d338..92ce31969e3 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -14,8 +14,10 @@ ShortcutsFindFile.__super__.constructor.call(this); _oldStopCallback = Mousetrap.stopCallback; Mousetrap.stopCallback = (function(_this) { + // override to fire shortcuts action when focus in textbox return function(event, element, combo) { if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) { + // when press up/down key in textbox, cusor prevent to move to home/end event.preventDefault(); return false; } diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 3f3a8a9dfd9..235bf4f95ec 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,7 +1,5 @@ /*= require mousetrap */ - - /*= require shortcuts_navigation */ (function() { @@ -43,16 +41,20 @@ if (selected.trim() === "") { return; } + // Put a '>' character before each non-empty line in the selection quote = _.map(selected.split("\n"), function(val) { if (val.trim() !== '') { return "> " + val + "\n"; } }); + // If replyField already has some content, add a newline before our quote separator = replyField.val().trim() !== "" && "\n" || ''; replyField.val(function(_, current) { return current + separator + quote.join('') + "\n"; }); + // Trigger autosave for the added text replyField.trigger('input'); + // Focus the input field return replyField.focus(); } }; diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index dba62638c78..2ae7bf5fc15 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,9 +1,20 @@ +// Syntax Highlighter +// +// Applies a syntax highlighting color scheme CSS class to any element with the +// `js-syntax-highlight` class +// +// ### Example Markup +// +// <div class="js-syntax-highlight"></div> +// (function() { $.fn.syntaxHighlight = function() { var $children; if ($(this).hasClass('js-syntax-highlight')) { + // Given the element itself, apply highlighting return $(this).addClass(gon.user_color_scheme); } else { + // Given a parent element, recurse to any of its applicable children $children = $(this).find('.js-syntax-highlight'); if ($children.length) { return $children.syntaxHighlight(); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 23eda7d44ca..93421649ac7 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -129,16 +129,21 @@ var currPage, currPages, newPages, pageParams, url; currPages = this.getTotalPages(); currPage = this.getCurrentPage(); + // Refresh if no remaining Todos if (!total) { location.reload(); return; } + // Do nothing if no pagination if (!currPages) { return; } newPages = Math.ceil(total / this.getTodosPerPage()); + // Includes query strings url = location.href; + // If new total of pages is different than we have now if (newPages !== currPages) { + // Redirect to previous page if there's one available if (currPages > 1 && currPage === currPages) { pageParams = { page: currPages - 1 @@ -155,6 +160,7 @@ if (!todoLink) { return; } + // Allow Meta-Click or Mouse3-click to open in a new tab if (e.metaKey || e.which === 2) { e.preventDefault(); return window.open(todoLink, '_blank'); diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 78e159a7ed9..9b7be17c4fe 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -2,6 +2,8 @@ this.TreeView = (function() { function TreeView() { this.initKeyNav(); + // Code browser tree slider + // Make the entire tree-item row clickable, but not if clicking another link (like a commit message) $(".tree-content-holder .tree-item").on('click', function(e) { var $clickedEl, path; $clickedEl = $(e.target); @@ -15,6 +17,7 @@ } } }); + // Show the "Loading commit data" for only the first element $('span.log_loading:first').removeClass('hide'); } diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 9ba847fb0c2..ce2930c7fc7 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,3 +1,7 @@ +// Authenticate U2F (universal 2nd factor) devices for users to authenticate with. +// +// State Flow #1: setup -> in_progress -> authenticated -> POST to server +// State Flow #2: setup -> in_progress -> error -> setup (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -15,6 +19,17 @@ this.appId = u2fParams.app_id; this.challenge = u2fParams.challenge; this.signRequests = u2fParams.sign_requests.map(function(request) { + // The U2F Javascript API v1.1 requires a single challenge, with + // _no challenges per-request_. The U2F Javascript API v1.0 requires a + // challenge per-request, which is done by copying the single challenge + // into every request. + // + // In either case, we don't need the per-request challenges that the server + // has generated, so we can remove them. + // + // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. + // This can be removed once we upgrade. + // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 return _(request).omit('challenge'); }); } @@ -41,6 +56,7 @@ })(this), 10); }; + // Rendering # U2FAuthenticate.prototype.templates = { "notSupported": "#js-authenticate-u2f-not-supported", "setup": '#js-authenticate-u2f-setup', @@ -75,6 +91,8 @@ U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { this.renderTemplate('authenticated'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. return this.container.find("#js-device-response").val(deviceResponse); }; diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index c87e0840df3..926912fa988 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,3 +1,7 @@ +// Register U2F (universal 2nd factor) devices for users to authenticate with. +// +// State Flow #1: setup -> in_progress -> registered -> POST to server +// State Flow #2: setup -> in_progress -> error -> setup (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -39,6 +43,7 @@ })(this), 10); }; + // Rendering # U2FRegister.prototype.templates = { "notSupported": "#js-register-u2f-not-supported", "setup": '#js-register-u2f-setup', @@ -73,6 +78,8 @@ U2FRegister.prototype.renderRegistered = function(deviceResponse) { this.renderTemplate('registered'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. return this.container.find("#js-device-response").val(deviceResponse); }; diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js index e5e75701fee..8a657780eb6 100644 --- a/app/assets/javascripts/user_tabs.js +++ b/app/assets/javascripts/user_tabs.js @@ -1,3 +1,61 @@ +// UserTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the Users#show page. +// +// ### Example Markup +// +// <ul class="nav-links"> +// <li class="activity-tab active"> +// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> +// Activity +// </a> +// </li> +// <li class="groups-tab"> +// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> +// Groups +// </a> +// </li> +// <li class="contributed-tab"> +// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> +// Contributed projects +// </a> +// </li> +// <li class="projects-tab"> +// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> +// Personal projects +// </a> +// </li> +// <li class="snippets-tab"> +// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="tab-pane" id="activity"> +// Activity Content +// </div> +// <div class="tab-pane" id="groups"> +// Groups Content +// </div> +// <div class="tab-pane" id="contributed"> +// Contributed projects content +// </div> +// <div class="tab-pane" id="projects"> +// Projects content +// </div> +// <div class="tab-pane" id="snippets"> +// Snippets content +// </div> +// </div> +// +// <div class="loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -6,18 +64,23 @@ this.tabShown = bind(this.tabShown, this); var i, item, len, ref, ref1, ref2, ref3; this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document); + // Make jQuery object if selector is provided if (typeof this.parentEl === 'string') { this.parentEl = $(this.parentEl); } + // Store the `location` object, allowing for easier stubbing in tests this._location = location; + // Set tab states this.loaded = {}; ref3 = this.parentEl.find('.nav-links a'); for (i = 0, len = ref3.length; i < len; i++) { item = ref3[i]; this.loaded[$(item).attr('data-action')] = false; } + // Actions this.actions = Object.keys(this.loaded); this.bindEvents(); + // Set active tab if (this.action === 'show') { this.action = this.defaultAction; } @@ -25,6 +88,7 @@ } UserTabs.prototype.bindEvents = function() { + // Toggle event listeners return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown); }; @@ -74,6 +138,7 @@ tabSelector = 'div#' + action; _this.parentEl.find(tabSelector).html(data.html); _this.loaded[action] = true; + // Fix tooltips return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); }; })(this) @@ -97,13 +162,17 @@ UserTabs.prototype.setCurrentAction = function(action) { var new_state, regExp; + // Remove possible actions from URL regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$'); new_state = this._location.pathname; + // remove trailing slashes new_state = new_state.replace(/\/+$/, ""); new_state = new_state.replace(regExp, ''); + // Append the new action if we're on a tab other than 'activity' if (action !== this.defaultAction) { new_state += "/" + action; } + // Ensure parameters and hash come along for the ride new_state += this._location.search + this._location.hash; history.replaceState({ turbolinks: true, diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 90cf551b32e..b8da7c4f297 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -11,6 +11,8 @@ this.daySizeWithSpace = this.daySize + (this.daySpace * 2); this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.months = []; + // Loop through the timestamps to create a group of objects + // The group of objects will be grouped based on the day of the week they are this.timestampsTmp = []; var group = 0; @@ -29,12 +31,15 @@ var day = date.getDay(); var count = timestamps[date.getTime() * 0.001]; + // Create a new group array if this is the first day of the week + // or if is first object if ((day === 0 && i !== 0) || i === 0) { this.timestampsTmp.push([]); group++; } var innerArray = this.timestampsTmp[group - 1]; + // Push to the inner array the values that will be used to render map innerArray.push({ count: count || 0, date: date, @@ -42,8 +47,10 @@ }); } + // Init color functions this.colorKey = this.initColorKey(); this.color = this.initColor(); + // Init the svg element this.renderSvg(group); this.renderDays(); this.renderMonths(); diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index b95faadc8e7..d6e4d9f7ad8 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index bad82868ab0..9c277998db4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -81,6 +81,7 @@ if (term.length === 0) { showDivider = 0; if (firstUser) { + // Move current user to the front of the list for (index = j = 0, len = users.length; j < len; index = ++j) { obj = users[index]; if (obj.username === firstUser) { @@ -115,6 +116,7 @@ if (showDivider) { users.splice(showDivider, 0, "divider"); } + // Send the data back return callback(users); }); }, @@ -139,6 +141,7 @@ inputId: 'issue_assignee_id', hidden: function(e) { $selectbox.hide(); + // display:block overrides the hide-collapse rule return $value.css('display', ''); }, clicked: function(user, $el, e) { @@ -177,6 +180,7 @@ img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />"; } } + // split into three parts so we can remove the username section if nessesary listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>"; listClosingTags = "</a> </li>"; @@ -215,6 +219,7 @@ }; if (query.term.length === 0) { if (firstUser) { + // Move current user to the front of the list ref = data.results; for (index = j = 0, len = ref.length; j < len; index = ++j) { obj = ref[index]; @@ -271,6 +276,7 @@ return _this.formatSelection.apply(_this, args); }, dropdownCssClass: "ajax-users-dropdown", + // we do not want to escape markup since we are displaying html in results escapeMarkup: function(m) { return m; } @@ -318,6 +324,8 @@ }); }; + // Return users list. Filtered by query + // Only active users retrieved UsersSelect.prototype.users = function(query, options, callback) { var url; url = this.buildUrl(this.usersPath); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 71236c6a27d..777b32b41c9 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,21 +1,34 @@ - +// Zen Mode (full screen) textarea +// /*= provides zen_mode:enter */ - - /*= provides zen_mode:leave */ - - +// /*= require jquery.scrollTo */ - - /*= require dropzone */ - - /*= require mousetrap */ - - /*= require mousetrap/pause */ +// +// ### Events +// +// `zen_mode:enter` +// +// Fired when the "Edit in fullscreen" link is clicked. +// +// **Synchronicity** Sync +// **Bubbles** Yes +// **Cancelable** No +// **Target** a.js-zen-enter +// +// `zen_mode:leave` +// +// Fired when the "Leave Fullscreen" link is clicked. +// +// **Synchronicity** Sync +// **Bubbles** Yes +// **Cancelable** No +// **Target** a.js-zen-leave +// (function() { this.ZenMode = (function() { function ZenMode() { @@ -40,6 +53,7 @@ }; })(this)); $(document).on('keydown', function(e) { + // Esc if (e.keyCode === 27) { e.preventDefault(); return $(document).trigger('zen_mode:leave'); @@ -52,6 +66,7 @@ this.active_backdrop = $(backdrop); this.active_backdrop.addClass('fullscreen'); this.active_textarea = this.active_backdrop.find('textarea'); + // Prevent a user-resized textarea from persisting to fullscreen this.active_textarea.removeAttr('style'); return this.active_textarea.focus(); }; |