diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2016-07-25 10:43:36 +0000 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2016-07-25 10:43:36 +0000 |
commit | fcb560f4bf5fdc606a962503bd2a061f138c162b (patch) | |
tree | 85cc23d000af4facae5a618ec5f764466dcf26c0 /app/assets | |
parent | 2d9e7468de7edfe1868b8d9dc6dcdaff116f0da8 (diff) | |
parent | 83180110348af1b244d56e9cdf5f29c5d6f84db0 (diff) | |
download | gitlab-ce-fcb560f4bf5fdc606a962503bd2a061f138c162b.tar.gz |
Merge remote-tracking branch 'upstream/master' into artifacts-from-ref-and-build-name
* upstream/master: (117 commits)
`WikiPage` should have a slug even when not persisted.
ES6ify all the things!
Make fork counter always clickable (!5463)
Revert "Merge branch '17073-tagscontroller-index-is-terrible-response-time-goes-up-to-5-…"
Fix CHANGELOG
Add spec for dashes in paths
Fix Error 500 when creating Wiki pages with hyphens or spaces
Add links to the real markdown.md file for all GFM examples
Remove magic comments from Ruby files (!5456)
Ignore invalid trusted proxies in X-Forwarded-For header
Fix bug where replies to commit notes displayed in the MR discussion tab wouldn't show up on the commit page
Improve project rename log messages for better debugging
If version file is unavailable unknown status
Add gitlab-workhorse version to admin dashboard
Add note to say GFM doesn't work on documentation website
Add detailed info on storage path mountpoints
Load project invited groups and members eagerly in ProjectTeam#fetch_members
fix missing repo_ref param
Add light border to rounded avatars
Gracefully handle case when keep-around references are corrupted or exist already
...
Diffstat (limited to 'app/assets')
281 files changed, 12792 insertions, 10479 deletions
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js new file mode 100644 index 00000000000..151455ce4a3 --- /dev/null +++ b/app/assets/javascripts/LabelManager.js @@ -0,0 +1,110 @@ +(function() { + this.LabelManager = (function() { + LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time'; + + function LabelManager(opts) { + var ref, ref1, ref2; + if (opts == null) { + opts = {}; + } + this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels'); + this.prioritizedLabels.sortable({ + items: 'li', + placeholder: 'list-placeholder', + axis: 'y', + update: this.onPrioritySortUpdate.bind(this) + }); + this.bindEvents(); + } + + LabelManager.prototype.bindEvents = function() { + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + }; + + LabelManager.prototype.onTogglePriorityClick = function(e) { + var $btn, $label, $tooltip, _this, action; + e.preventDefault(); + _this = e.data; + $btn = $(e.currentTarget); + $label = $("#" + ($btn.data('domId'))); + action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby'))); + $tooltip.tooltip('destroy'); + return _this.toggleLabelPriority($label, action); + }; + + LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { + var $from, $target, _this, url, xhr; + if (persistState == null) { + persistState = true; + } + _this = this; + url = $label.find('.js-toggle-priority').data('url'); + $target = this.prioritizedLabels; + $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + if ($from.find('li').length === 1) { + $from.find('.empty-message').removeClass('hidden'); + } + if (!$target.find('li').length) { + $target.find('.empty-message').addClass('hidden'); + } + $label.detach().appendTo($target); + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url: url, + type: 'DELETE' + }); + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); + } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + }; + + LabelManager.prototype.onPrioritySortUpdate = function() { + var xhr; + xhr = this.savePrioritySort(); + return xhr.fail(function() { + return new Flash(this.errorMessage, 'alert'); + }); + }; + + LabelManager.prototype.savePrioritySort = function() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + }; + + LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { + var action; + action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); + }; + + LabelManager.prototype.getSortedLabelsIds = function() { + var sortedIds; + sortedIds = []; + this.prioritizedLabels.find('li').each(function() { + return sortedIds.push($(this).data('id')); + }); + return sortedIds; + }; + + return LabelManager; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee deleted file mode 100644 index 6d8faba40d7..00000000000 --- a/app/assets/javascripts/LabelManager.js.coffee +++ /dev/null @@ -1,92 +0,0 @@ -class @LabelManager - errorMessage: 'Unable to update label prioritization at this time' - - constructor: (opts = {}) -> - # Defaults - { - @togglePriorityButton = $('.js-toggle-priority') - @prioritizedLabels = $('.js-prioritized-labels') - @otherLabels = $('.js-other-labels') - } = opts - - @prioritizedLabels.sortable( - items: 'li' - placeholder: 'list-placeholder' - axis: 'y' - update: @onPrioritySortUpdate.bind(@) - ) - - @bindEvents() - - bindEvents: -> - @togglePriorityButton.on 'click', @, @onTogglePriorityClick - - onTogglePriorityClick: (e) -> - e.preventDefault() - _this = e.data - $btn = $(e.currentTarget) - $label = $("##{$btn.data('domId')}") - action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' - - # Make sure tooltip will hide - $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}" - $tooltip.tooltip 'destroy' - - _this.toggleLabelPriority($label, action) - - toggleLabelPriority: ($label, action, persistState = true) -> - _this = @ - url = $label.find('.js-toggle-priority').data 'url' - - $target = @prioritizedLabels - $from = @otherLabels - - # Optimistic update - if action is 'remove' - $target = @otherLabels - $from = @prioritizedLabels - - if $from.find('li').length is 1 - $from.find('.empty-message').removeClass('hidden') - - if not $target.find('li').length - $target.find('.empty-message').addClass('hidden') - - $label.detach().appendTo($target) - - # Return if we are not persisting state - return unless persistState - - if action is 'remove' - xhr = $.ajax url: url, type: 'DELETE' - - # Restore empty message - $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length - else - xhr = @savePrioritySort($label, action) - - xhr.fail @rollbackLabelPosition.bind(@, $label, action) - - onPrioritySortUpdate: -> - xhr = @savePrioritySort() - - xhr.fail -> - new Flash(@errorMessage, 'alert') - - savePrioritySort: () -> - $.post - url: @prioritizedLabels.data('url') - data: - label_ids: @getSortedLabelsIds() - - rollbackLabelPosition: ($label, originalAction)-> - action = if originalAction is 'remove' then 'add' else 'remove' - @toggleLabelPriority($label, action, false) - - new Flash(@errorMessage, 'alert') - - getSortedLabelsIds: -> - sortedIds = [] - @prioritizedLabels.find('li').each -> - sortedIds.push $(@).data 'id' - sortedIds diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js new file mode 100644 index 00000000000..1ab3c2197d8 --- /dev/null +++ b/app/assets/javascripts/activities.js @@ -0,0 +1,40 @@ +(function() { + this.Activities = (function() { + function Activities() { + Pager.init(20, true, false, this.updateTooltips); + $(".event-filter-link").on("click", (function(_this) { + return function(event) { + event.preventDefault(); + _this.toggleFilter($(event.currentTarget)); + return _this.reloadActivities(); + }; + })(this)); + } + + Activities.prototype.updateTooltips = function() { + return gl.utils.localTimeAgo($('.js-timeago', '#activity')); + }; + + Activities.prototype.reloadActivities = function() { + $(".content_list").html(''); + return Pager.init(20, true); + }; + + Activities.prototype.toggleFilter = function(sender) { + var event_filters, filter; + $('.event-filter .active').removeClass("active"); + event_filters = $.cookie("event_filter"); + filter = sender.attr("id").split("_")[0]; + $.cookie("event_filter", (event_filters !== filter ? filter : ""), { + path: '/' + }); + if (event_filters !== filter) { + return sender.closest('li').toggleClass("active"); + } + }; + + return Activities; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee deleted file mode 100644 index ed5a5d0260c..00000000000 --- a/app/assets/javascripts/activities.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -class @Activities - constructor: -> - Pager.init 20, true, false, @updateTooltips - $(".event-filter-link").on "click", (event) => - event.preventDefault() - @toggleFilter($(event.currentTarget)) - @reloadActivities() - - updateTooltips: -> - gl.utils.localTimeAgo($('.js-timeago', '#activity')) - - reloadActivities: -> - $(".content_list").html '' - Pager.init 20, true - - - toggleFilter: (sender) -> - $('.event-filter .active').removeClass "active" - event_filters = $.cookie("event_filter") - filter = sender.attr("id").split("_")[0] - $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' } - - if event_filters isnt filter - sender.closest('li').toggleClass "active" diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js new file mode 100644 index 00000000000..f8460beb5d2 --- /dev/null +++ b/app/assets/javascripts/admin.js @@ -0,0 +1,64 @@ +(function() { + this.Admin = (function() { + function Admin() { + var modal, showBlacklistType; + $('input#user_force_random_password').on('change', function(elem) { + var elems; + elems = $('#user_password, #user_password_confirmation'); + if ($(this).attr('checked')) { + return elems.val('').attr('disabled', true); + } else { + return elems.removeAttr('disabled'); + } + }); + $('body').on('click', '.js-toggle-colors-link', function(e) { + e.preventDefault(); + return $('.js-toggle-colors-container').toggle(); + }); + $('.log-tabs a').click(function(e) { + e.preventDefault(); + return $(this).tab('show'); + }); + $('.log-bottom').click(function(e) { + var visible_log; + e.preventDefault(); + visible_log = $(".file-content:visible"); + return visible_log.animate({ + scrollTop: visible_log.find('ol').height() + }, "fast"); + }); + modal = $('.change-owner-holder'); + $('.change-owner-link').bind("click", function(e) { + e.preventDefault(); + $(this).hide(); + return modal.show(); + }); + $('.change-owner-cancel-link').bind("click", function(e) { + e.preventDefault(); + modal.hide(); + return $('.change-owner-link').show(); + }); + $('li.project_member').bind('ajax:success', function() { + return Turbolinks.visit(location.href); + }); + $('li.group_member').bind('ajax:success', function() { + return Turbolinks.visit(location.href); + }); + showBlacklistType = function() { + if ($("input[name='blacklist_type']:checked").val() === 'file') { + $('.blacklist-file').show(); + return $('.blacklist-raw').hide(); + } else { + $('.blacklist-file').hide(); + return $('.blacklist-raw').show(); + } + }; + $("input[name='blacklist_type']").click(showBlacklistType); + showBlacklistType(); + } + + return Admin; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee deleted file mode 100644 index b2b8e1b7ffb..00000000000 --- a/app/assets/javascripts/admin.js.coffee +++ /dev/null @@ -1,40 +0,0 @@ -class @Admin - constructor: -> - $('input#user_force_random_password').on 'change', (elem) -> - elems = $('#user_password, #user_password_confirmation') - - if $(@).attr 'checked' - elems.val('').attr 'disabled', true - else - elems.removeAttr 'disabled' - - $('body').on 'click', '.js-toggle-colors-link', (e) -> - e.preventDefault() - $('.js-toggle-colors-container').toggle() - - $('.log-tabs a').click (e) -> - e.preventDefault() - $(this).tab('show') - - $('.log-bottom').click (e) -> - e.preventDefault() - visible_log = $(".file-content:visible") - visible_log.animate({ scrollTop: visible_log.find('ol').height() }, "fast") - - modal = $('.change-owner-holder') - - $('.change-owner-link').bind "click", (e) -> - e.preventDefault() - $(this).hide() - modal.show() - - $('.change-owner-cancel-link').bind "click", (e) -> - e.preventDefault() - modal.hide() - $('.change-owner-link').show() - - $('li.project_member').bind 'ajax:success', -> - Turbolinks.visit(location.href) - - $('li.group_member').bind 'ajax:success', -> - Turbolinks.visit(location.href) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js new file mode 100644 index 00000000000..49c2ac0dac3 --- /dev/null +++ b/app/assets/javascripts/api.js @@ -0,0 +1,136 @@ +(function() { + this.Api = { + groupsPath: "/api/:version/groups.json", + groupPath: "/api/:version/groups/:id.json", + namespacesPath: "/api/:version/namespaces.json", + groupProjectsPath: "/api/:version/groups/:id/projects.json", + projectsPath: "/api/:version/projects.json?simple=true", + labelsPath: "/api/:version/projects/:id/labels", + licensePath: "/api/:version/licenses/:key", + gitignorePath: "/api/:version/gitignores/:key", + gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", + group: function(group_id, callback) { + var url; + url = Api.buildUrl(Api.groupPath); + url = url.replace(':id', group_id); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token + }, + dataType: "json" + }).done(function(group) { + return callback(group); + }); + }, + groups: function(query, skip_ldap, callback) { + var url; + url = Api.buildUrl(Api.groupsPath); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(groups) { + return callback(groups); + }); + }, + namespaces: function(query, callback) { + var url; + url = Api.buildUrl(Api.namespacesPath); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(namespaces) { + return callback(namespaces); + }); + }, + projects: function(query, order, callback) { + var url; + url = Api.buildUrl(Api.projectsPath); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + order_by: order, + per_page: 20 + }, + dataType: "json" + }).done(function(projects) { + return callback(projects); + }); + }, + newLabel: function(project_id, data, callback) { + var url; + url = Api.buildUrl(Api.labelsPath); + url = url.replace(':id', project_id); + data.private_token = gon.api_token; + return $.ajax({ + url: url, + type: "POST", + data: data, + dataType: "json" + }).done(function(label) { + return callback(label); + }).error(function(message) { + return callback(message.responseJSON); + }); + }, + groupProjects: function(group_id, query, callback) { + var url; + url = Api.buildUrl(Api.groupProjectsPath); + url = url.replace(':id', group_id); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(projects) { + return callback(projects); + }); + }, + licenseText: function(key, data, callback) { + var url; + url = Api.buildUrl(Api.licensePath).replace(':key', key); + return $.ajax({ + url: url, + data: data + }).done(function(license) { + return callback(license); + }); + }, + gitignoreText: function(key, callback) { + var url; + url = Api.buildUrl(Api.gitignorePath).replace(':key', key); + return $.get(url, function(gitignore) { + return callback(gitignore); + }); + }, + gitlabCiYml: function(key, callback) { + var url; + url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); + return $.get(url, function(file) { + return callback(file); + }); + }, + buildUrl: function(url) { + if (gon.relative_url_root != null) { + url = gon.relative_url_root + url; + } + return url.replace(':version', gon.api_version); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee deleted file mode 100644 index 89b0ac697ed..00000000000 --- a/app/assets/javascripts/api.js.coffee +++ /dev/null @@ -1,122 +0,0 @@ -@Api = - groupsPath: "/api/:version/groups.json" - groupPath: "/api/:version/groups/:id.json" - namespacesPath: "/api/:version/namespaces.json" - groupProjectsPath: "/api/:version/groups/:id/projects.json" - projectsPath: "/api/:version/projects.json?simple=true" - labelsPath: "/api/:version/projects/:id/labels" - licensePath: "/api/:version/licenses/:key" - gitignorePath: "/api/:version/gitignores/:key" - gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key" - - group: (group_id, callback) -> - url = Api.buildUrl(Api.groupPath) - url = url.replace(':id', group_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - dataType: "json" - ).done (group) -> - callback(group) - - # Return groups list. Filtered by query - # Only active groups retrieved - groups: (query, skip_ldap, callback) -> - url = Api.buildUrl(Api.groupsPath) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - dataType: "json" - ).done (groups) -> - callback(groups) - - # Return namespaces list. Filtered by query - namespaces: (query, callback) -> - url = Api.buildUrl(Api.namespacesPath) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - dataType: "json" - ).done (namespaces) -> - callback(namespaces) - - # Return projects list. Filtered by query - projects: (query, order, callback) -> - url = Api.buildUrl(Api.projectsPath) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - order_by: order - per_page: 20 - dataType: "json" - ).done (projects) -> - callback(projects) - - newLabel: (project_id, data, callback) -> - url = Api.buildUrl(Api.labelsPath) - url = url.replace(':id', project_id) - - data.private_token = gon.api_token - $.ajax( - url: url - type: "POST" - data: data - dataType: "json" - ).done (label) -> - callback(label) - .error (message) -> - callback(message.responseJSON) - - # Return group projects list. Filtered by query - groupProjects: (group_id, query, callback) -> - url = Api.buildUrl(Api.groupProjectsPath) - url = url.replace(':id', group_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - dataType: "json" - ).done (projects) -> - callback(projects) - - # Return text for a specific license - licenseText: (key, data, callback) -> - url = Api.buildUrl(Api.licensePath).replace(':key', key) - - $.ajax( - url: url - data: data - ).done (license) -> - callback(license) - - gitignoreText: (key, callback) -> - url = Api.buildUrl(Api.gitignorePath).replace(':key', key) - - $.get url, (gitignore) -> - callback(gitignore) - - gitlabCiYml: (key, callback) -> - url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key) - - $.get url, (file) -> - callback(file) - - buildUrl: (url) -> - url = gon.relative_url_root + url if gon.relative_url_root? - return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 00000000000..127e568adc9 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,320 @@ +/*= require jquery2 */ +/*= require jquery-ui/autocomplete */ +/*= require jquery-ui/datepicker */ +/*= require jquery-ui/draggable */ +/*= require jquery-ui/effect-highlight */ +/*= require jquery-ui/sortable */ +/*= require jquery_ujs */ +/*= require jquery.cookie */ +/*= require jquery.endless-scroll */ +/*= require jquery.highlight */ +/*= require jquery.waitforimages */ +/*= require jquery.atwho */ +/*= require jquery.scrollTo */ +/*= require jquery.turbolinks */ +/*= require turbolinks */ +/*= require autosave */ +/*= require bootstrap/affix */ +/*= require bootstrap/alert */ +/*= require bootstrap/button */ +/*= require bootstrap/collapse */ +/*= require bootstrap/dropdown */ +/*= require bootstrap/modal */ +/*= require bootstrap/scrollspy */ +/*= require bootstrap/tab */ +/*= require bootstrap/transition */ +/*= require bootstrap/tooltip */ +/*= require bootstrap/popover */ +/*= require select2 */ +/*= require ace/ace */ +/*= require ace/ext-searchbox */ +/*= require underscore */ +/*= require dropzone */ +/*= require mousetrap */ +/*= require mousetrap/pause */ +/*= require shortcuts */ +/*= require shortcuts_navigation */ +/*= require shortcuts_dashboard_navigation */ +/*= require shortcuts_issuable */ +/*= require shortcuts_network */ +/*= require jquery.nicescroll */ +/*= require date.format */ +/*= require_directory ./behaviors */ +/*= require_directory ./blob */ +/*= require_directory ./commit */ +/*= require_directory ./extensions */ +/*= require_directory ./lib/utils */ +/*= require_directory ./u2f */ +/*= require_directory . */ +/*= require fuzzaldrin-plus */ + +(function() { + window.slugify = function(text) { + return text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase(); + }; + + window.ajaxGet = function(url) { + return $.ajax({ + type: "GET", + url: url, + dataType: "script" + }); + }; + + window.split = function(val) { + return val.split(/,\s*/); + }; + + window.extractLast = function(term) { + return split(term).pop(); + }; + + window.rstrip = function(val) { + if (val) { + return val.replace(/\s+$/, ''); + } else { + return val; + } + }; + + window.disableButtonIfEmptyField = function(field_selector, button_selector) { + var closest_submit, field; + field = $(field_selector); + closest_submit = field.closest('form').find(button_selector); + if (rstrip(field.val()) === "") { + closest_submit.disable(); + } + return field.on('input', function() { + if (rstrip($(this).val()) === "") { + return closest_submit.disable(); + } else { + return closest_submit.enable(); + } + }); + }; + + window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) { + var closest_submit, updateButtons; + closest_submit = form.find(button_selector); + updateButtons = function() { + var filled; + filled = true; + form.find('input').filter(form_selector).each(function() { + return filled = rstrip($(this).val()) !== "" || !$(this).attr('required'); + }); + if (filled) { + return closest_submit.enable(); + } else { + return closest_submit.disable(); + } + }; + updateButtons(); + return form.keyup(updateButtons); + }; + + window.sanitize = function(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); + }; + + window.unbindEvents = function() { + return $(document).off('scroll'); + }; + + window.shiftWindow = function() { + return scrollBy(0, -100); + }; + + document.addEventListener("page:fetch", unbindEvents); + + window.addEventListener("hashchange", shiftWindow); + + window.onload = function() { + if (location.hash) { + return setTimeout(shiftWindow, 100); + } + }; + + $(function() { + var $body, $document, $sidebarGutterToggle, $window, bootstrapBreakpoint, checkInitialSidebarSize, fitSidebarForSize, flash; + $document = $(document); + $window = $(window); + $body = $('body'); + gl.utils.preventDisabledButtons(); + bootstrapBreakpoint = bp.getBreakpointSize(); + $(".nav-sidebar").niceScroll({ + cursoropacitymax: '0.4', + cursorcolor: '#FFF', + cursorborder: "1px solid #FFF" + }); + $(".js-select-on-focus").on("focusin", function() { + return $(this).select().one('mouseup', function(e) { + return e.preventDefault(); + }); + }); + $('.remove-row').bind('ajax:success', function() { + return $(this).closest('li').fadeOut(); + }); + $('.js-remove-tr').bind('ajax:before', function() { + return $(this).hide(); + }); + $('.js-remove-tr').bind('ajax:success', function() { + return $(this).closest('tr').fadeOut(); + }); + $('select.select2').select2({ + width: 'resolve', + dropdownAutoWidth: true + }); + $('.js-select2').bind('select2-close', function() { + return setTimeout((function() { + $('.select2-container-active').removeClass('select2-container-active'); + return $(':focus').blur(); + }), 1); + }); + $body.tooltip({ + selector: '.has-tooltip, [data-toggle="tooltip"]', + placement: function(_, el) { + var $el; + $el = $(el); + return $el.data('placement') || 'bottom'; + } + }); + $('.trigger-submit').on('change', function() { + return $(this).parents('form').submit(); + }); + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + if ((flash = $(".flash-container")).length > 0) { + flash.click(function() { + return $(this).fadeOut(); + }); + flash.show(); + } + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) { + var buttons; + buttons = $('[type="submit"]', this); + switch (e.type) { + case 'ajax:beforeSend': + case 'submit': + return buttons.disable(); + default: + return buttons.enable(); + } + }); + $(document).ajaxError(function(e, xhrObj, xhrSetting, xhrErrorText) { + var ref; + if (xhrObj.status === 401) { + return new Flash('You need to be logged in.', 'alert'); + } else if ((ref = xhrObj.status) === 404 || ref === 500) { + return new Flash('Something went wrong on our end.', 'alert'); + } + }); + $('.account-box').hover(function() { + return $(this).toggleClass('hover'); + }); + $document.on('click', '.diff-content .js-show-suppressed-diff', function() { + var $container; + $container = $(this).parent(); + $container.next('table').show(); + return $container.remove(); + }); + $('.navbar-toggle').on('click', function() { + $('.header-content .title').toggle(); + $('.header-content .header-logo').toggle(); + $('.header-content .navbar-collapse').toggle(); + return $('.navbar-toggle').toggleClass('active'); + }); + $body.on("click", ".js-toggle-diff-comments", function(e) { + $(this).toggleClass('active'); + $(this).closest(".diff-file").find(".notes_holder").toggle(); + return e.preventDefault(); + }); + $document.off("click", '.js-confirm-danger'); + $document.on("click", '.js-confirm-danger', function(e) { + var btn, form, text; + e.preventDefault(); + btn = $(e.target); + text = btn.data("confirm-danger-message"); + form = btn.closest("form"); + return new ConfirmDangerModal(form, text); + }); + $document.on('click', 'button', function() { + return $(this).blur(); + }); + $('input[type="search"]').each(function() { + var $this; + $this = $(this); + $this.attr('value', $this.val()); + }); + $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function(e) { + var $this; + $this = $(this); + return $this.attr('value', $this.val()); + }); + $sidebarGutterToggle = $('.js-sidebar-toggle'); + $document.off('breakpoint:change').on('breakpoint:change', function(e, breakpoint) { + var $gutterIcon; + if (breakpoint === 'sm' || breakpoint === 'xs') { + $gutterIcon = $sidebarGutterToggle.find('i'); + if ($gutterIcon.hasClass('fa-angle-double-right')) { + return $sidebarGutterToggle.trigger('click'); + } + } + }); + fitSidebarForSize = function() { + var oldBootstrapBreakpoint; + oldBootstrapBreakpoint = bootstrapBreakpoint; + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + checkInitialSidebarSize = function() { + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint === "xs" || "sm") { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + $window.off("resize.app").on("resize.app", function(e) { + return fitSidebarForSize(); + }); + gl.awardsHandler = new AwardsHandler(); + checkInitialSidebarSize(); + new Aside(); + if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') { + $.cookie('pin_nav', 'false', { + path: '/', + expires: 365 * 10 + }); + $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned'); + $('.navbar-fixed-top').removeClass('header-pinned-nav'); + } + return $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) { + var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText; + e.preventDefault(); + $pinBtn = $(e.currentTarget); + $page = $('.page-with-sidebar'); + $topNav = $('.navbar-fixed-top'); + $tooltip = $("#" + ($pinBtn.attr('aria-describedby'))); + doPinNav = !$page.is('.page-sidebar-pinned'); + tooltipText = 'Pin navigation'; + $(this).toggleClass('is-active'); + if (doPinNav) { + $page.addClass('page-sidebar-pinned'); + $topNav.addClass('header-pinned-nav'); + } else { + $tooltip.remove(); + $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded'); + $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded'); + } + $.cookie('pin_nav', doPinNav, { + path: '/', + expires: 365 * 10 + }); + if ($.cookie('pin_nav') === 'true' || doPinNav) { + tooltipText = 'Unpin navigation'; + } + $tooltip.find('.tooltip-inner').text(tooltipText); + return $pinBtn.attr('title', tooltipText).tooltip('fixTitle'); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee deleted file mode 100644 index eceff6d91d5..00000000000 --- a/app/assets/javascripts/application.js.coffee +++ /dev/null @@ -1,310 +0,0 @@ -# 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 -#= require jquery-ui/draggable -#= require jquery-ui/effect-highlight -#= require jquery-ui/sortable -#= require jquery_ujs -#= require jquery.cookie -#= require jquery.endless-scroll -#= require jquery.highlight -#= require jquery.waitforimages -#= require jquery.atwho -#= require jquery.scrollTo -#= require jquery.turbolinks -#= require turbolinks -#= require autosave -#= require bootstrap/affix -#= require bootstrap/alert -#= require bootstrap/button -#= require bootstrap/collapse -#= require bootstrap/dropdown -#= require bootstrap/modal -#= require bootstrap/scrollspy -#= require bootstrap/tab -#= require bootstrap/transition -#= require bootstrap/tooltip -#= require bootstrap/popover -#= require select2 -#= require ace/ace -#= require ace/ext-searchbox -#= require underscore -#= require dropzone -#= require mousetrap -#= require mousetrap/pause -#= require shortcuts -#= require shortcuts_navigation -#= require shortcuts_dashboard_navigation -#= require shortcuts_issuable -#= require shortcuts_network -#= require jquery.nicescroll -#= require date.format -#= require_directory ./behaviors -#= require_directory ./blob -#= require_directory ./commit -#= require_directory ./extensions -#= require_directory ./lib/utils -#= require_directory ./u2f -#= require_directory . -#= require fuzzaldrin-plus - -window.slugify = (text) -> - text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() - -window.ajaxGet = (url) -> - $.ajax({type: "GET", url: url, dataType: "script"}) - -window.split = (val) -> - return val.split( /,\s*/ ) - -window.extractLast = (term) -> - return split( term ).pop() - -window.rstrip = (val) -> - return if val then val.replace(/\s+$/, '') else val - -# Disable button if text field is empty -window.disableButtonIfEmptyField = (field_selector, button_selector) -> - field = $(field_selector) - closest_submit = field.closest('form').find(button_selector) - - closest_submit.disable() if rstrip(field.val()) is "" - - field.on 'input', -> - if rstrip($(@).val()) is "" - closest_submit.disable() - else - closest_submit.enable() - -# Disable button if any input field with given selector is empty -window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> - closest_submit = form.find(button_selector) - updateButtons = -> - filled = true - form.find('input').filter(form_selector).each -> - filled = rstrip($(this).val()) != "" || !$(this).attr('required') - - if filled - closest_submit.enable() - else - closest_submit.disable() - - updateButtons() - form.keyup(updateButtons) - -window.sanitize = (str) -> - return str.replace(/<(?:.|\n)*?>/gm, '') - -window.unbindEvents = -> - $(document).off('scroll') - -window.shiftWindow = -> - scrollBy 0, -100 - -document.addEventListener("page:fetch", unbindEvents) - -window.addEventListener "hashchange", shiftWindow - -window.onload = -> - # Scroll the window to avoid the topnav bar - # https://github.com/twitter/bootstrap/issues/1768 - if location.hash - setTimeout shiftWindow, 100 - -$ -> - - $document = $(document) - $window = $(window) - $body = $('body') - - gl.utils.preventDisabledButtons() - bootstrapBreakpoint = bp.getBreakpointSize() - - $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") - - # Click a .js-select-on-focus field, select the contents - $(".js-select-on-focus").on "focusin", -> - # Prevent a mouseup event from deselecting the input - $(this).select().one 'mouseup', (e) -> - e.preventDefault() - - $('.remove-row').bind 'ajax:success', -> - $(this).closest('li').fadeOut() - - $('.js-remove-tr').bind 'ajax:before', -> - $(this).hide() - - $('.js-remove-tr').bind 'ajax:success', -> - $(this).closest('tr').fadeOut() - - # Initialize select2 selects - $('select.select2').select2(width: 'resolve', dropdownAutoWidth: true) - - # Close select2 on escape - $('.js-select2').bind 'select2-close', -> - setTimeout ( -> - $('.select2-container-active').removeClass('select2-container-active') - $(':focus').blur() - ), 1 - - # Initialize tooltips - $body.tooltip( - selector: '.has-tooltip, [data-toggle="tooltip"]' - placement: (_, el) -> - $el = $(el) - $el.data('placement') || 'bottom' - ) - - # Form submitter - $('.trigger-submit').on 'change', -> - $(@).parents('form').submit() - - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true) - - # Flash - if (flash = $(".flash-container")).length > 0 - flash.click -> $(@).fadeOut() - flash.show() - - # Disable form buttons while a form is submitting - $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) -> - buttons = $('[type="submit"]', @) - - switch e.type - when 'ajax:beforeSend', 'submit' - buttons.disable() - else - buttons.enable() - - $(document).ajaxError (e, xhrObj, xhrSetting, xhrErrorText) -> - - if xhrObj.status is 401 - new Flash 'You need to be logged in.', 'alert' - - else if xhrObj.status in [ 404, 500 ] - new Flash 'Something went wrong on our end.', 'alert' - - - # Show/Hide the profile menu when hovering the account box - $('.account-box').hover -> $(@).toggleClass('hover') - - # Commit show suppressed diff - $document.on 'click', '.diff-content .js-show-suppressed-diff', -> - $container = $(@).parent() - $container.next('table').show() - $container.remove() - - $('.navbar-toggle').on 'click', -> - $('.header-content .title').toggle() - $('.header-content .header-logo').toggle() - $('.header-content .navbar-collapse').toggle() - $('.navbar-toggle').toggleClass('active') - - # Show/hide comments on diff - $body.on "click", ".js-toggle-diff-comments", (e) -> - $(@).toggleClass('active') - $(@).closest(".diff-file").find(".notes_holder").toggle() - e.preventDefault() - - $document.off "click", '.js-confirm-danger' - $document.on "click", '.js-confirm-danger', (e) -> - e.preventDefault() - btn = $(e.target) - text = btn.data("confirm-danger-message") - form = btn.closest("form") - new ConfirmDangerModal(form, text) - - - $document.on 'click', 'button', -> - $(this).blur() - - $('input[type="search"]').each -> - $this = $(this) - $this.attr 'value', $this.val() - return - - $document - .off 'keyup', 'input[type="search"]' - .on 'keyup', 'input[type="search"]' , (e) -> - $this = $(this) - $this.attr 'value', $this.val() - - $sidebarGutterToggle = $('.js-sidebar-toggle') - - $document - .off 'breakpoint:change' - .on 'breakpoint:change', (e, breakpoint) -> - if breakpoint is 'sm' or breakpoint is 'xs' - $gutterIcon = $sidebarGutterToggle.find('i') - if $gutterIcon.hasClass('fa-angle-double-right') - $sidebarGutterToggle.trigger('click') - - fitSidebarForSize = -> - oldBootstrapBreakpoint = bootstrapBreakpoint - bootstrapBreakpoint = bp.getBreakpointSize() - if bootstrapBreakpoint != oldBootstrapBreakpoint - $document.trigger('breakpoint:change', [bootstrapBreakpoint]) - - checkInitialSidebarSize = -> - bootstrapBreakpoint = bp.getBreakpointSize() - if bootstrapBreakpoint is "xs" or "sm" - $document.trigger('breakpoint:change', [bootstrapBreakpoint]) - - $window - .off "resize.app" - .on "resize.app", (e) -> - fitSidebarForSize() - - gl.awardsHandler = new AwardsHandler() - checkInitialSidebarSize() - new Aside() - - # Sidenav pinning - if $window.width() < 1024 and $.cookie('pin_nav') is 'true' - $.cookie('pin_nav', 'false', { path: '/', expires: 365 * 10 }) - $('.page-with-sidebar') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - .removeClass('page-sidebar-pinned') - $('.navbar-fixed-top').removeClass('header-pinned-nav') - - $document - .off 'click', '.js-nav-pin' - .on 'click', '.js-nav-pin', (e) -> - e.preventDefault() - - $pinBtn = $(e.currentTarget) - $page = $ '.page-with-sidebar' - $topNav = $ '.navbar-fixed-top' - $tooltip = $ "##{$pinBtn.attr('aria-describedby')}" - doPinNav = not $page.is('.page-sidebar-pinned') - tooltipText = 'Pin navigation' - - $(this).toggleClass 'is-active' - - if doPinNav - $page.addClass('page-sidebar-pinned') - $topNav.addClass('header-pinned-nav') - else - $tooltip.remove() # Remove it immediately when collapsing the sidebar - $page.removeClass('page-sidebar-pinned') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - $topNav.removeClass('header-pinned-nav') - .toggleClass('header-collapsed header-expanded') - - # Save settings - $.cookie 'pin_nav', doPinNav, { path: '/', expires: 365 * 10 } - - if $.cookie('pin_nav') is 'true' or doPinNav - tooltipText = 'Unpin navigation' - - # Update tooltip text immediately - $tooltip.find('.tooltip-inner').text(tooltipText) - - # Persist tooltip title - $pinBtn.attr('title', tooltipText).tooltip('fixTitle') diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js new file mode 100644 index 00000000000..7b546e79ee0 --- /dev/null +++ b/app/assets/javascripts/aside.js @@ -0,0 +1,26 @@ +(function() { + this.Aside = (function() { + function Aside() { + $(document).off("click", "a.show-aside"); + $(document).on("click", 'a.show-aside', function(e) { + var btn, icon; + e.preventDefault(); + btn = $(e.currentTarget); + icon = btn.find('i'); + if (icon.hasClass('fa-angle-left')) { + btn.parent().find('section').hide(); + btn.parent().find('aside').fadeIn(); + return icon.removeClass('fa-angle-left').addClass('fa-angle-right'); + } else { + btn.parent().find('aside').hide(); + btn.parent().find('section').fadeIn(); + return icon.removeClass('fa-angle-right').addClass('fa-angle-left'); + } + }); + } + + return Aside; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee deleted file mode 100644 index 66ab5054326..00000000000 --- a/app/assets/javascripts/aside.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -class @Aside - constructor: -> - $(document).off "click", "a.show-aside" - $(document).on "click", 'a.show-aside', (e) -> - e.preventDefault() - btn = $(e.currentTarget) - icon = btn.find('i') - - if icon.hasClass('fa-angle-left') - btn.parent().find('section').hide() - btn.parent().find('aside').fadeIn() - icon.removeClass('fa-angle-left').addClass('fa-angle-right') - else - btn.parent().find('aside').hide() - btn.parent().find('section').fadeIn() - icon.removeClass('fa-angle-right').addClass('fa-angle-left') diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js new file mode 100644 index 00000000000..7116512d6b7 --- /dev/null +++ b/app/assets/javascripts/autosave.js @@ -0,0 +1,63 @@ +(function() { + this.Autosave = (function() { + function Autosave(field, key) { + this.field = field; + if (key.join != null) { + key = key.join("/"); + } + this.key = "autosave/" + key; + this.field.data("autosave", this); + this.restore(); + this.field.on("input", (function(_this) { + return function() { + return _this.save(); + }; + })(this)); + } + + Autosave.prototype.restore = function() { + var e, error, text; + if (window.localStorage == null) { + return; + } + try { + text = window.localStorage.getItem(this.key); + } catch (error) { + e = error; + return; + } + if ((text != null ? text.length : void 0) > 0) { + this.field.val(text); + } + return this.field.trigger("input"); + }; + + Autosave.prototype.save = function() { + var text; + if (window.localStorage == null) { + return; + } + text = this.field.val(); + if ((text != null ? text.length : void 0) > 0) { + try { + return window.localStorage.setItem(this.key, text); + } catch (undefined) {} + } else { + return this.reset(); + } + }; + + Autosave.prototype.reset = function() { + if (window.localStorage == null) { + return; + } + try { + return window.localStorage.removeItem(this.key); + } catch (undefined) {} + }; + + return Autosave; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee deleted file mode 100644 index 28f8e103664..00000000000 --- a/app/assets/javascripts/autosave.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -class @Autosave - constructor: (field, key) -> - @field = field - - key = key.join("/") if key.join? - @key = "autosave/#{key}" - - @field.data "autosave", this - - @restore() - - @field.on "input", => @save() - - restore: -> - return unless window.localStorage? - - try - text = window.localStorage.getItem @key - catch e - return - - @field.val text if text?.length > 0 - @field.trigger "input" - - save: -> - return unless window.localStorage? - - text = @field.val() - if text?.length > 0 - try - window.localStorage.setItem @key, text - else - @reset() - - reset: -> - return unless window.localStorage? - - try - window.localStorage.removeItem @key diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee deleted file mode 100644 index 37d0adaa625..00000000000 --- a/app/assets/javascripts/awards_handler.coffee +++ /dev/null @@ -1,372 +0,0 @@ -class @AwardsHandler - - constructor: -> - - @aliases = gl.emojiAliases() - - $(document) - .off 'click', '.js-add-award' - .on 'click', '.js-add-award', (e) => - e.stopPropagation() - e.preventDefault() - - @showEmojiMenu $(e.currentTarget) - - $('html').on 'click', (e) -> - $target = $ e.target - - unless $target.closest('.emoji-menu-content').length - $('.js-awards-block.current').removeClass 'current' - - unless $target.closest('.emoji-menu').length - if $('.emoji-menu').is(':visible') - $('.js-add-award.is-active').removeClass 'is-active' - $('.emoji-menu').removeClass 'is-visible' - - $(document) - .off 'click', '.js-emoji-btn' - .on 'click', '.js-emoji-btn', (e) => - e.preventDefault() - - $target = $ e.currentTarget - emoji = $target.find('.icon').data 'emoji' - - $target.closest('.js-awards-block').addClass 'current' - @addAward @getVotesBlock(), @getAwardUrl(), emoji - - - showEmojiMenu: ($addBtn) -> - - $menu = $ '.emoji-menu' - - if $addBtn.hasClass 'js-note-emoji' - $addBtn.closest('.note').find('.js-awards-block').addClass 'current' - else - $addBtn.closest('.js-awards-block').addClass 'current' - - if $menu.length - $holder = $addBtn.closest('.js-award-holder') - - if $menu.is '.is-visible' - $addBtn.removeClass 'is-active' - $menu.removeClass 'is-visible' - $('#emoji_search').blur() - else - $addBtn.addClass 'is-active' - @positionMenu($menu, $addBtn) - - $menu.addClass 'is-visible' - $('#emoji_search').focus() - else - $addBtn.addClass 'is-loading is-active' - url = @getAwardMenuUrl() - - @createEmojiMenu url, => - $addBtn.removeClass 'is-loading' - $menu = $('.emoji-menu') - @positionMenu($menu, $addBtn) - @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered - - setTimeout => - $menu.addClass 'is-visible' - $('#emoji_search').focus() - @setupSearch() - , 200 - - - createEmojiMenu: (awardMenuUrl, callback) -> - - $.get awardMenuUrl, (response) -> - $('body').append response - callback() - - - positionMenu: ($menu, $addBtn) -> - - 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" - - if position? and position is 'right' - css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" - $menu.addClass 'is-aligned-right' - else - css.left = "#{$addBtn.offset().left}px" - $menu.removeClass 'is-aligned-right' - - $menu.css(css) - - - addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) -> - - emoji = @normilizeEmojiName emoji - - @postEmoji awardUrl, emoji, => - @addAwardToEmojiBar votesBlock, emoji, checkMutuality - callback?() - - $('.emoji-menu').removeClass 'is-visible' - - - addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) -> - - @checkMutuality votesBlock, emoji if checkForMutuality - @addEmojiToFrequentlyUsedList emoji - - emoji = @normilizeEmojiName emoji - $emojiButton = @findEmojiIcon(votesBlock, emoji).parent() - - if $emojiButton.length > 0 - if @isActive $emojiButton - @decrementCounter $emojiButton, emoji - else - counter = $emojiButton.find '.js-counter' - counter.text parseInt(counter.text()) + 1 - $emojiButton.addClass 'active' - @addMeToUserList votesBlock, emoji - @animateEmoji $emojiButton - else - votesBlock.removeClass 'hidden' - @createEmoji votesBlock, emoji - - - getVotesBlock: -> - - currentBlock = $ '.js-awards-block.current' - return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0 - - - getAwardUrl: -> return @getVotesBlock().data 'award-url' - - - checkMutuality: (votesBlock, emoji) -> - - awardUrl = @getAwardUrl() - - if emoji in [ 'thumbsup', 'thumbsdown' ] - mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' - $emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent() - isAlreadyVoted = $emojiButton.hasClass 'active' - - if isAlreadyVoted - @showEmojiLoader $emojiButton - @addAward votesBlock, awardUrl, mutualVote, false, -> - $emojiButton.removeClass 'is-loading' - - - showEmojiLoader: ($emojiButton) -> - - $loader = $emojiButton.find '.fa-spinner' - - unless $loader.length - $emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>' - - $emojiButton.addClass 'is-loading' - - - isActive: ($emojiButton) -> $emojiButton.hasClass 'active' - - - decrementCounter: ($emojiButton, emoji) -> - - counter = $ '.js-counter', $emojiButton - counterNumber = parseInt counter.text(), 10 - - if counterNumber > 1 - counter.text counterNumber - 1 - @removeMeFromUserList $emojiButton, emoji - else if emoji is 'thumbsup' or emoji is 'thumbsdown' - $emojiButton.tooltip 'destroy' - counter.text '0' - @removeMeFromUserList $emojiButton, emoji - @removeEmoji $emojiButton if $emojiButton.parents('.note').length - else - @removeEmoji $emojiButton - - $emojiButton.removeClass 'active' - - - removeEmoji: ($emojiButton) -> - - $emojiButton.tooltip('destroy') - $emojiButton.remove() - - $votesBlock = @getVotesBlock() - - if $votesBlock.find('.js-emoji-btn').length is 0 - $votesBlock.addClass 'hidden' - - - getAwardTooltip: ($awardBlock) -> - - return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or '' - - - removeMeFromUserList: ($emojiButton, emoji) -> - - awardBlock = $emojiButton - originalTitle = @getAwardTooltip awardBlock - - authors = originalTitle.split ', ' - authors.splice authors.indexOf('me'), 1 - - newAuthors = authors.join ', ' - - awardBlock - .closest '.js-emoji-btn' - .removeData 'original-title' - .attr 'data-original-title', newAuthors - - @resetTooltip awardBlock - - - addMeToUserList: (votesBlock, emoji) -> - - awardBlock = @findEmojiIcon(votesBlock, emoji).parent() - origTitle = @getAwardTooltip awardBlock - users = [] - - if origTitle - users = origTitle.trim().split ', ' - - users.push 'me' - awardBlock.attr 'title', users.join ', ' - - @resetTooltip awardBlock - - - resetTooltip: (award) -> - - award.tooltip 'destroy' - - # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. - cb = -> award.tooltip() - setTimeout cb, 200 - - - createEmoji_: (votesBlock, emoji) -> - - emojiCssClass = @resolveNameToCssClass emoji - buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> - <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div> - <span class='award-control-text js-counter'>1</span> - </button>" - - $emojiButton = $ buttonHtml - $emojiButton - .insertBefore votesBlock.find '.js-award-holder' - .find '.emoji-icon' - .data 'emoji', emoji - - @animateEmoji $emojiButton - $('.award-control').tooltip() - votesBlock.removeClass 'current' - - - animateEmoji: ($emoji) -> - - className = 'pulse animated' - - $emoji.addClass className - setTimeout (-> $emoji.removeClass className), 321 - - - createEmoji: (votesBlock, emoji) -> - - if $('.emoji-menu').length - return @createEmoji_ votesBlock, emoji - - @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji - - - getAwardMenuUrl: -> return gon.award_menu_url - - - resolveNameToCssClass: (emoji) -> - - emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']" - - 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}" - - - postEmoji: (awardUrl, emoji, callback) -> - - $.post awardUrl, { name: emoji }, (data) -> - callback() if data.ok - - - findEmojiIcon: (votesBlock, emoji) -> - - return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']" - - - scrollToAwards: -> - - options = scrollTop: $('.awards').offset().top - 110 - $('body, html').animate options, 200 - - - normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji - - - addEmojiToFrequentlyUsedList: (emoji) -> - - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - frequentlyUsedEmojis.push emoji - $.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 } - - - getFrequentlyUsedEmojis: -> - - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',') - return _.compact _.uniq frequentlyUsedEmojis - - - renderFrequentlyUsedBlock: -> - - if $.cookie 'frequently_used_emojis' - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - - ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>") - - for emoji in frequentlyUsedEmojis - $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) - - $('.emoji-menu-content') - .prepend(ul) - .prepend($('<h5>').text('Frequently used')) - - @frequentEmojiBlockRendered = true - - - setupSearch: -> - - $('input.emoji-search').on 'keyup', (ev) => - 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 = @searchEmojis(term).show() - ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis) - $('.emoji-menu-content ul, .emoji-menu-content h5').hide() - $('.emoji-menu-content').append(h5).append(ul) - else - $('.emoji-menu-content').children().show() - - - searchEmojis: (term) -> - - $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone() diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js new file mode 100644 index 00000000000..ea683b31f75 --- /dev/null +++ b/app/assets/javascripts/awards_handler.js @@ -0,0 +1,380 @@ +(function() { + this.AwardsHandler = (function() { + function AwardsHandler() { + this.aliases = gl.emojiAliases(); + $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { + return function(e) { + e.stopPropagation(); + e.preventDefault(); + return _this.showEmojiMenu($(e.currentTarget)); + }; + })(this)); + $('html').on('click', function(e) { + var $target; + $target = $(e.target); + if (!$target.closest('.emoji-menu-content').length) { + $('.js-awards-block.current').removeClass('current'); + } + if (!$target.closest('.emoji-menu').length) { + if ($('.emoji-menu').is(':visible')) { + $('.js-add-award.is-active').removeClass('is-active'); + return $('.emoji-menu').removeClass('is-visible'); + } + } + }); + $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) { + return function(e) { + var $target, emoji; + e.preventDefault(); + $target = $(e.currentTarget); + emoji = $target.find('.icon').data('emoji'); + $target.closest('.js-awards-block').addClass('current'); + return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji); + }; + })(this)); + } + + AwardsHandler.prototype.showEmojiMenu = function($addBtn) { + var $holder, $menu, url; + $menu = $('.emoji-menu'); + if ($addBtn.hasClass('js-note-emoji')) { + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); + } else { + $addBtn.closest('.js-awards-block').addClass('current'); + } + if ($menu.length) { + $holder = $addBtn.closest('.js-award-holder'); + if ($menu.is('.is-visible')) { + $addBtn.removeClass('is-active'); + $menu.removeClass('is-visible'); + return $('#emoji_search').blur(); + } else { + $addBtn.addClass('is-active'); + this.positionMenu($menu, $addBtn); + $menu.addClass('is-visible'); + return $('#emoji_search').focus(); + } + } else { + $addBtn.addClass('is-loading is-active'); + url = this.getAwardMenuUrl(); + return this.createEmojiMenu(url, (function(_this) { + return function() { + $addBtn.removeClass('is-loading'); + $menu = $('.emoji-menu'); + _this.positionMenu($menu, $addBtn); + if (!_this.frequentEmojiBlockRendered) { + _this.renderFrequentlyUsedBlock(); + } + return setTimeout(function() { + $menu.addClass('is-visible'); + $('#emoji_search').focus(); + return _this.setupSearch(); + }, 200); + }; + })(this)); + } + }; + + AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) { + return $.get(awardMenuUrl, function(response) { + $('body').append(response); + return callback(); + }); + }; + + AwardsHandler.prototype.positionMenu = function($menu, $addBtn) { + var css, position; + position = $addBtn.data('position'); + css = { + top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" + }; + if ((position != null) && position === 'right') { + css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px"; + $menu.addClass('is-aligned-right'); + } else { + css.left = ($addBtn.offset().left) + "px"; + $menu.removeClass('is-aligned-right'); + } + return $menu.css(css); + }; + + AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) { + if (checkMutuality == null) { + checkMutuality = true; + } + emoji = this.normilizeEmojiName(emoji); + this.postEmoji(awardUrl, emoji, (function(_this) { + return function() { + _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality); + return typeof callback === "function" ? callback() : void 0; + }; + })(this)); + return $('.emoji-menu').removeClass('is-visible'); + }; + + AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) { + var $emojiButton, counter; + if (checkForMutuality == null) { + checkForMutuality = true; + } + if (checkForMutuality) { + this.checkMutuality(votesBlock, emoji); + } + this.addEmojiToFrequentlyUsedList(emoji); + emoji = this.normilizeEmojiName(emoji); + $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent(); + if ($emojiButton.length > 0) { + if (this.isActive($emojiButton)) { + return this.decrementCounter($emojiButton, emoji); + } else { + counter = $emojiButton.find('.js-counter'); + counter.text(parseInt(counter.text()) + 1); + $emojiButton.addClass('active'); + this.addMeToUserList(votesBlock, emoji); + return this.animateEmoji($emojiButton); + } + } else { + votesBlock.removeClass('hidden'); + return this.createEmoji(votesBlock, emoji); + } + }; + + AwardsHandler.prototype.getVotesBlock = function() { + var currentBlock; + currentBlock = $('.js-awards-block.current'); + if (currentBlock.length) { + return currentBlock; + } else { + return $('.js-awards-block').eq(0); + } + }; + + AwardsHandler.prototype.getAwardUrl = function() { + return this.getVotesBlock().data('award-url'); + }; + + AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) { + var $emojiButton, awardUrl, isAlreadyVoted, mutualVote; + awardUrl = this.getAwardUrl(); + if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent(); + isAlreadyVoted = $emojiButton.hasClass('active'); + if (isAlreadyVoted) { + this.showEmojiLoader($emojiButton); + return this.addAward(votesBlock, awardUrl, mutualVote, false, function() { + return $emojiButton.removeClass('is-loading'); + }); + } + } + }; + + AwardsHandler.prototype.showEmojiLoader = function($emojiButton) { + var $loader; + $loader = $emojiButton.find('.fa-spinner'); + if (!$loader.length) { + $emojiButton.append('<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>'); + } + return $emojiButton.addClass('is-loading'); + }; + + AwardsHandler.prototype.isActive = function($emojiButton) { + return $emojiButton.hasClass('active'); + }; + + AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) { + var counter, counterNumber; + counter = $('.js-counter', $emojiButton); + counterNumber = parseInt(counter.text(), 10); + if (counterNumber > 1) { + counter.text(counterNumber - 1); + this.removeMeFromUserList($emojiButton, emoji); + } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + $emojiButton.tooltip('destroy'); + counter.text('0'); + this.removeMeFromUserList($emojiButton, emoji); + if ($emojiButton.parents('.note').length) { + this.removeEmoji($emojiButton); + } + } else { + this.removeEmoji($emojiButton); + } + return $emojiButton.removeClass('active'); + }; + + AwardsHandler.prototype.removeEmoji = function($emojiButton) { + var $votesBlock; + $emojiButton.tooltip('destroy'); + $emojiButton.remove(); + $votesBlock = this.getVotesBlock(); + if ($votesBlock.find('.js-emoji-btn').length === 0) { + return $votesBlock.addClass('hidden'); + } + }; + + AwardsHandler.prototype.getAwardTooltip = function($awardBlock) { + return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; + }; + + AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) { + var authors, awardBlock, newAuthors, originalTitle; + awardBlock = $emojiButton; + originalTitle = this.getAwardTooltip(awardBlock); + authors = originalTitle.split(', '); + authors.splice(authors.indexOf('me'), 1); + newAuthors = authors.join(', '); + awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors); + return this.resetTooltip(awardBlock); + }; + + AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) { + var awardBlock, origTitle, users; + awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); + origTitle = this.getAwardTooltip(awardBlock); + users = []; + if (origTitle) { + users = origTitle.trim().split(', '); + } + users.push('me'); + awardBlock.attr('title', users.join(', ')); + return this.resetTooltip(awardBlock); + }; + + AwardsHandler.prototype.resetTooltip = function(award) { + var cb; + award.tooltip('destroy'); + cb = function() { + return award.tooltip(); + }; + return setTimeout(cb, 200); + }; + + AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) { + var $emojiButton, buttonHtml, emojiCssClass; + emojiCssClass = this.resolveNameToCssClass(emoji); + buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>"; + $emojiButton = $(buttonHtml); + $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji); + this.animateEmoji($emojiButton); + $('.award-control').tooltip(); + return votesBlock.removeClass('current'); + }; + + AwardsHandler.prototype.animateEmoji = function($emoji) { + var className; + className = 'pulse animated'; + $emoji.addClass(className); + return setTimeout((function() { + return $emoji.removeClass(className); + }), 321); + }; + + AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) { + if ($('.emoji-menu').length) { + return this.createEmoji_(votesBlock, emoji); + } + return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) { + return function() { + return _this.createEmoji_(votesBlock, emoji); + }; + })(this)); + }; + + AwardsHandler.prototype.getAwardMenuUrl = function() { + return gon.award_menu_url; + }; + + AwardsHandler.prototype.resolveNameToCssClass = function(emoji) { + var emojiIcon, unicodeName; + emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']"); + if (emojiIcon.length > 0) { + unicodeName = emojiIcon.data('unicode-name'); + } else { + unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name'); + } + return "emoji-" + unicodeName; + }; + + AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) { + return $.post(awardUrl, { + name: emoji + }, function(data) { + if (data.ok) { + return callback(); + } + }); + }; + + AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) { + return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']"); + }; + + AwardsHandler.prototype.scrollToAwards = function() { + var options; + options = { + scrollTop: $('.awards').offset().top - 110 + }; + return $('body, html').animate(options, 200); + }; + + AwardsHandler.prototype.normilizeEmojiName = function(emoji) { + return this.aliases[emoji] || emoji; + }; + + AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) { + var frequentlyUsedEmojis; + frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + frequentlyUsedEmojis.push(emoji); + return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { + expires: 365 + }); + }; + + AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { + var frequentlyUsedEmojis; + frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(','); + return _.compact(_.uniq(frequentlyUsedEmojis)); + }; + + AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { + var emoji, frequentlyUsedEmojis, i, len, ul; + if ($.cookie('frequently_used_emojis')) { + frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); + for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) { + emoji = frequentlyUsedEmojis[i]; + $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul); + } + $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used')); + } + return this.frequentEmojiBlockRendered = true; + }; + + AwardsHandler.prototype.setupSearch = function() { + return $('input.emoji-search').on('keyup', (function(_this) { + return function(ev) { + var found_emojis, h5, term, ul; + term = $(ev.target).val(); + $('ul.emoji-menu-search, h5.emoji-search').remove(); + if (term) { + h5 = $('<h5>').text('Search results'); + found_emojis = _this.searchEmojis(term).show(); + ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + return $('.emoji-menu-content').append(h5).append(ul); + } else { + return $('.emoji-menu-content').children().show(); + } + }; + })(this)); + }; + + AwardsHandler.prototype.searchEmojis = function(term) { + return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone(); + }; + + return AwardsHandler; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js new file mode 100644 index 00000000000..f977a1e8a7b --- /dev/null +++ b/app/assets/javascripts/behaviors/autosize.js @@ -0,0 +1,30 @@ + +/*= require jquery.ba-resize */ + + +/*= require autosize */ + +(function() { + $(function() { + var $fields; + $fields = $('.js-autosize'); + $fields.on('autosize:resized', function() { + var $field; + $field = $(this); + return $field.data('height', $field.outerHeight()); + }); + $fields.on('resize.autosize', function() { + var $field; + $field = $(this); + if ($field.data('height') !== $field.outerHeight()) { + $field.data('height', $field.outerHeight()); + autosize.destroy($field); + return $field.css('max-height', window.outerHeight); + } + }); + autosize($fields); + autosize.update($fields); + return $fields.css('resize', 'vertical'); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee deleted file mode 100644 index a072fe48a98..00000000000 --- a/app/assets/javascripts/behaviors/autosize.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -#= require jquery.ba-resize -#= require autosize - -$ -> - $fields = $('.js-autosize') - - $fields.on 'autosize:resized', -> - $field = $(@) - $field.data('height', $field.outerHeight()) - - $fields.on 'resize.autosize', -> - $field = $(@) - - if $field.data('height') != $field.outerHeight() - $field.data('height', $field.outerHeight()) - autosize.destroy($field) - $field.css('max-height', window.outerHeight) - - autosize($fields) - autosize.update($fields) - - $fields.css('resize', 'vertical') diff --git a/app/assets/javascripts/behaviors/details_behavior.coffee b/app/assets/javascripts/behaviors/details_behavior.coffee deleted file mode 100644 index decab3e1bed..00000000000 --- a/app/assets/javascripts/behaviors/details_behavior.coffee +++ /dev/null @@ -1,15 +0,0 @@ -$ -> - $("body").on "click", ".js-details-target", -> - container = $(@).closest(".js-details-container") - container.toggleClass("open") - - # Show details content. Hides link after click. - # - # %div - # %a.js-details-expand - # %div.js-details-content - # - $("body").on "click", ".js-details-expand", (e) -> - $(@).next('.js-details-content').removeClass("hide") - $(@).hide() - e.preventDefault() diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js new file mode 100644 index 00000000000..3631d1b74ac --- /dev/null +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -0,0 +1,15 @@ +(function() { + $(function() { + $("body").on("click", ".js-details-target", function() { + var container; + container = $(this).closest(".js-details-container"); + return container.toggleClass("open"); + }); + return $("body").on("click", ".js-details-expand", function(e) { + $(this).next('.js-details-content').removeClass("hide"); + $(this).hide(); + return e.preventDefault(); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js new file mode 100644 index 00000000000..3527d0a95fc --- /dev/null +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -0,0 +1,58 @@ + +/*= require extensions/jquery */ + +(function() { + var isMac, keyCodeIs; + + isMac = function() { + return navigator.userAgent.match(/Macintosh/); + }; + + keyCodeIs = function(e, keyCode) { + if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) { + return false; + } + return e.keyCode === keyCode; + }; + + $(document).on('keydown.quick_submit', '.js-quick-submit', function(e) { + var $form, $submit_button; + if (!keyCodeIs(e, 13)) { + return; + } + if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) { + return; + } + e.preventDefault(); + $form = $(e.target).closest('form'); + $submit_button = $form.find('input[type=submit], button[type=submit]'); + if ($submit_button.attr('disabled')) { + return; + } + $submit_button.disable(); + return $form.submit(); + }); + + $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) { + var $this, title; + if (!keyCodeIs(e, 9)) { + return; + } + if (isMac()) { + title = "You can also press ⌘-Enter"; + } else { + title = "You can also press Ctrl-Enter"; + } + $this = $(this); + return $this.tooltip({ + container: 'body', + html: 'true', + placement: 'auto top', + title: title, + trigger: 'manual' + }).tooltip('show').one('blur', function() { + return $this.tooltip('hide'); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee deleted file mode 100644 index 3cb96bacaa7..00000000000 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -# 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> -# -isMac = -> - navigator.userAgent.match(/Macintosh/) - -keyCodeIs = (e, keyCode) -> - return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat - return e.keyCode == keyCode - -$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> - return unless keyCodeIs(e, 13) # Enter - - return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) - - e.preventDefault() - - $form = $(e.target).closest('form') - $submit_button = $form.find('input[type=submit], button[type=submit]') - - return if $submit_button.attr('disabled') - - $submit_button.disable() - $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]', (e) -> - return unless keyCodeIs(e, 9) # Tab - - if isMac() - title = "You can also press ⌘-Enter" - else - title = "You can also press Ctrl-Enter" - - $this = $(@) - $this.tooltip( - container: 'body' - html: 'true' - placement: 'auto top' - title: title - trigger: 'manual' - ).tooltip('show').one('blur', -> $this.tooltip('hide')) diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js new file mode 100644 index 00000000000..db0b36b24e9 --- /dev/null +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -0,0 +1,45 @@ + +/*= require extensions/jquery */ + +(function() { + $.fn.requiresInput = function() { + var $button, $form, fieldSelector, requireInput, required; + $form = $(this); + $button = $('button[type=submit], input[type=submit]', $form); + required = '[required=required]'; + fieldSelector = "input" + required + ", select" + required + ", textarea" + required; + requireInput = function() { + var values; + values = _.map($(fieldSelector, $form), function(field) { + return field.value; + }); + if (values.length && _.any(values, _.isEmpty)) { + return $button.disable(); + } else { + return $button.enable(); + } + }; + requireInput(); + return $form.on('change input', fieldSelector, requireInput); + }; + + $(function() { + var $form, hideOrShowHelpBlock; + $form = $('form.js-requires-input'); + $form.requiresInput(); + hideOrShowHelpBlock = function(form) { + var selected; + selected = $('.js-select-namespace option:selected'); + if (selected.length && selected.data('options-parent') === 'groups') { + return form.find('.help-block').hide(); + } else if (selected.length) { + return form.find('.help-block').show(); + } + }; + hideOrShowHelpBlock($form); + return $('.select2.js-select-namespace').change(function() { + return hideOrShowHelpBlock($form); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee deleted file mode 100644 index 0faa570ce13..00000000000 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ -# 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> -# -$.fn.requiresInput = -> - $form = $(this) - $button = $('button[type=submit], input[type=submit]', $form) - - required = '[required=required]' - fieldSelector = "input#{required}, select#{required}, textarea#{required}" - - requireInput = -> - # Collect the input values of *all* required fields - values = _.map $(fieldSelector, $form), (field) -> field.value - - # Disable the button if any required fields are empty - if values.length && _.any(values, _.isEmpty) - $button.disable() - else - $button.enable() - - # Set initial button state - requireInput() - - $form.on 'change input', fieldSelector, requireInput - -$ -> - $form = $('form.js-requires-input') - $form.requiresInput() - - # Hide or Show the help block when creating a new project - # based on the option selected - hideOrShowHelpBlock = (form) -> - selected = $('.js-select-namespace option:selected') - if selected.length and selected.data('options-parent') is 'groups' - return form.find('.help-block').hide() - else if selected.length - form.find('.help-block').show() - - hideOrShowHelpBlock($form) - - $('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee deleted file mode 100644 index 177b6918270..00000000000 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ /dev/null @@ -1,14 +0,0 @@ -$ -> - # 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", (e) -> - $(@).find('i'). - toggleClass('fa fa-chevron-down'). - toggleClass('fa fa-chevron-up') - $(@).closest(".js-toggle-container").find(".js-toggle-content").toggle() - e.preventDefault() diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js new file mode 100644 index 00000000000..1b7b63489ea --- /dev/null +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -0,0 +1,10 @@ +(function() { + $(function() { + return $("body").on("click", ".js-toggle-button", function(e) { + $(this).find('i').toggleClass('fa fa-chevron-down').toggleClass('fa fa-chevron-up'); + $(this).closest(".js-toggle-container").find(".js-toggle-content").toggle(); + return e.preventDefault(); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js new file mode 100644 index 00000000000..68758574967 --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js @@ -0,0 +1,46 @@ + +/*= require blob/template_selector */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.BlobCiYamlSelector = (function(superClass) { + extend(BlobCiYamlSelector, superClass); + + function BlobCiYamlSelector() { + return BlobCiYamlSelector.__super__.constructor.apply(this, arguments); + } + + BlobCiYamlSelector.prototype.requestFile = function(query) { + return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); + }; + + return BlobCiYamlSelector; + + })(TemplateSelector); + + this.BlobCiYamlSelectors = (function() { + function BlobCiYamlSelectors(opts) { + var ref; + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor; + this.$dropdowns.each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new BlobCiYamlSelector({ + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown, + editor: _this.editor + }); + }; + })(this)); + } + + return BlobCiYamlSelectors; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee deleted file mode 100644 index d9a03d05529..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require blob/template_selector - -class @BlobCiYamlSelector extends TemplateSelector - requestFile: (query) -> - Api.gitlabCiYml query.name, @requestFileSuccess.bind(@) - -class @BlobCiYamlSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-gitlab-ci-yml-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobCiYamlSelector( - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown, - editor: @editor - ) diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js new file mode 100644 index 00000000000..f4044f22db2 --- /dev/null +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -0,0 +1,62 @@ +(function() { + this.BlobFileDropzone = (function() { + function BlobFileDropzone(form, method) { + var dropzone, form_dropzone, submitButton; + form_dropzone = form.find('.dropzone'); + Dropzone.autoDiscover = false; + dropzone = form_dropzone.dropzone({ + autoDiscover: false, + autoProcessQueue: false, + url: form.attr('action'), + method: method, + clickable: true, + uploadMultiple: false, + paramName: "file", + maxFilesize: gon.max_file_size || 10, + parallelUploads: 1, + maxFiles: 1, + addRemoveLinks: true, + previewsContainer: '.dropzone-previews', + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + init: function() { + this.on('addedfile', function(file) { + $('.dropzone-alerts').html('').hide(); + }); + this.on('success', function(header, response) { + window.location.href = response.filePath; + }); + this.on('maxfilesexceeded', function(file) { + this.removeFile(file); + }); + return this.on('sending', function(file, xhr, formData) { + formData.append('target_branch', form.find('.js-target-branch').val()); + formData.append('create_merge_request', form.find('.js-create-merge-request').val()); + formData.append('commit_message', form.find('.js-commit-message').val()); + }); + }, + error: function(file, errorMessage) { + var stripped; + stripped = $("<div/>").html(errorMessage).text(); + $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show(); + this.removeFile(file); + } + }); + submitButton = form.find('#submit-all')[0]; + submitButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (dropzone[0].dropzone.getQueuedFiles().length === 0) { + alert("Please select a file"); + } + dropzone[0].dropzone.processQueue(); + return false; + }); + } + + return BlobFileDropzone; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee deleted file mode 100644 index 9df932817f6..00000000000 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ /dev/null @@ -1,57 +0,0 @@ -class @BlobFileDropzone - constructor: (form, method) -> - form_dropzone = form.find('.dropzone') - Dropzone.autoDiscover = false - dropzone = form_dropzone.dropzone( - 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 - paramName: "file" - maxFilesize: gon.max_file_size or 10 - parallelUploads: 1 - maxFiles: 1 - addRemoveLinks: true - previewsContainer: '.dropzone-previews' - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - init: -> - this.on 'addedfile', (file) -> - $('.dropzone-alerts').html('').hide() - - return - - this.on 'success', (header, response) -> - window.location.href = response.filePath - return - - this.on 'maxfilesexceeded', (file) -> - @removeFile file - return - - this.on 'sending', (file, xhr, formData) -> - formData.append('target_branch', form.find('.js-target-branch').val()) - formData.append('create_merge_request', form.find('.js-create-merge-request').val()) - formData.append('commit_message', form.find('.js-commit-message').val()) - return - - # Override behavior of adding error underneath preview - error: (file, errorMessage) -> - stripped = $("<div/>").html(errorMessage).text(); - $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show() - @removeFile file - return - ) - - submitButton = form.find('#submit-all')[0] - submitButton.addEventListener 'click', (e) -> - e.preventDefault() - e.stopPropagation() - alert "Please select a file" if dropzone[0].dropzone.getQueuedFiles().length == 0 - dropzone[0].dropzone.processQueue() - return false diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js new file mode 100644 index 00000000000..54a09e919f8 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -0,0 +1,23 @@ + +/*= require blob/template_selector */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.BlobGitignoreSelector = (function(superClass) { + extend(BlobGitignoreSelector, superClass); + + function BlobGitignoreSelector() { + return BlobGitignoreSelector.__super__.constructor.apply(this, arguments); + } + + BlobGitignoreSelector.prototype.requestFile = function(query) { + return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this)); + }; + + return BlobGitignoreSelector; + + })(TemplateSelector); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee deleted file mode 100644 index 8d0e3f363d1..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -#= require blob/template_selector - -class @BlobGitignoreSelector extends TemplateSelector - requestFile: (query) -> - Api.gitignoreText query.name, @requestFileSuccess.bind(@) diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js new file mode 100644 index 00000000000..4e9500428b2 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -0,0 +1,25 @@ +(function() { + this.BlobGitignoreSelectors = (function() { + function BlobGitignoreSelectors(opts) { + var ref; + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor; + this.$dropdowns.each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new BlobGitignoreSelector({ + pattern: /(.gitignore)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), + dropdown: $dropdown, + editor: _this.editor + }); + }; + })(this)); + } + + return BlobGitignoreSelectors; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee deleted file mode 100644 index a719ba25122..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -class @BlobGitignoreSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-gitignore-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobGitignoreSelector( - pattern: /(.gitignore)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), - dropdown: $dropdown, - editor: @editor - ) diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js new file mode 100644 index 00000000000..9a8ef08f4e5 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -0,0 +1,28 @@ + +/*= require blob/template_selector */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.BlobLicenseSelector = (function(superClass) { + extend(BlobLicenseSelector, superClass); + + function BlobLicenseSelector() { + return BlobLicenseSelector.__super__.constructor.apply(this, arguments); + } + + BlobLicenseSelector.prototype.requestFile = function(query) { + var data; + data = { + project: this.dropdown.data('project'), + fullname: this.dropdown.data('fullname') + }; + return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this)); + }; + + return BlobLicenseSelector; + + })(TemplateSelector); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee deleted file mode 100644 index a3cc8dd844c..00000000000 --- a/app/assets/javascripts/blob/blob_license_selector.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -#= require blob/template_selector - -class @BlobLicenseSelector extends TemplateSelector - requestFile: (query) -> - data = - project: @dropdown.data('project') - fullname: @dropdown.data('fullname') - - Api.licenseText query.id, data, @requestFileSuccess.bind(@) diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js new file mode 100644 index 00000000000..39237705e8d --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selectors.js @@ -0,0 +1,25 @@ +(function() { + this.BlobLicenseSelectors = (function() { + function BlobLicenseSelectors(opts) { + var ref; + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor; + this.$dropdowns.each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new BlobLicenseSelector({ + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + editor: _this.editor + }); + }; + })(this)); + } + + return BlobLicenseSelectors; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee deleted file mode 100644 index 68438733108..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -class @BlobLicenseSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-license-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobLicenseSelector( - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - editor: @editor - ) diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob/edit_blob.js new file mode 100644 index 00000000000..649c79daee8 --- /dev/null +++ b/app/assets/javascripts/blob/edit_blob.js @@ -0,0 +1,66 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.EditBlob = (function() { + function EditBlob(assets_path, ace_mode) { + if (ace_mode == null) { + ace_mode = null; + } + this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this); + ace.config.set("modePath", assets_path + "/ace"); + ace.config.loadModule("ace/ext/searchbox"); + this.editor = ace.edit("editor"); + this.editor.focus(); + if (ace_mode) { + this.editor.getSession().setMode("ace/mode/" + ace_mode); + } + $('form').submit((function(_this) { + return function() { + return $("#file-content").val(_this.editor.getValue()); + }; + })(this)); + this.initModePanesAndLinks(); + new BlobLicenseSelectors({ + editor: this.editor + }); + new BlobGitignoreSelectors({ + editor: this.editor + }); + new BlobCiYamlSelectors({ + editor: this.editor + }); + } + + EditBlob.prototype.initModePanesAndLinks = function() { + this.$editModePanes = $(".js-edit-mode-pane"); + this.$editModeLinks = $(".js-edit-mode a"); + return this.$editModeLinks.click(this.editModeLinkClickHandler); + }; + + EditBlob.prototype.editModeLinkClickHandler = function(event) { + var currentLink, currentPane, paneId; + event.preventDefault(); + currentLink = $(event.target); + paneId = currentLink.attr("href"); + currentPane = this.$editModePanes.filter(paneId); + this.$editModeLinks.parent().removeClass("active hover"); + currentLink.parent().addClass("active hover"); + this.$editModePanes.hide(); + currentPane.fadeIn(200); + if (paneId === "#preview") { + return $.post(currentLink.data("preview-url"), { + content: this.editor.getValue() + }, function(response) { + currentPane.empty().append(response); + return currentPane.syntaxHighlight(); + }); + } else { + return this.editor.focus(); + } + }; + + return EditBlob; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee deleted file mode 100644 index 19e584519d7..00000000000 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -class @EditBlob - constructor: (assets_path, ace_mode = null) -> - ace.config.set "modePath", "#{assets_path}/ace" - ace.config.loadModule "ace/ext/searchbox" - @editor = ace.edit("editor") - @editor.focus() - @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode - - # Before a form submission, move the content from the Ace editor into the - # submitted textarea - $('form').submit => - $("#file-content").val(@editor.getValue()) - - @initModePanesAndLinks() - - new BlobLicenseSelectors { @editor } - new BlobGitignoreSelectors { @editor } - new BlobCiYamlSelectors { @editor } - - initModePanesAndLinks: -> - @$editModePanes = $(".js-edit-mode-pane") - @$editModeLinks = $(".js-edit-mode a") - @$editModeLinks.click @editModeLinkClickHandler - - editModeLinkClickHandler: (event) => - event.preventDefault() - currentLink = $(event.target) - paneId = currentLink.attr("href") - currentPane = @$editModePanes.filter(paneId) - @$editModeLinks.parent().removeClass "active hover" - currentLink.parent().addClass "active hover" - @$editModePanes.hide() - currentPane.fadeIn 200 - if paneId is "#preview" - $.post currentLink.data("preview-url"), - content: @editor.getValue() - , (response) -> - currentPane.empty().append response - currentPane.syntaxHighlight() - - else - @editor.focus() diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js new file mode 100644 index 00000000000..2cf0a6631b8 --- /dev/null +++ b/app/assets/javascripts/blob/template_selector.js @@ -0,0 +1,74 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.TemplateSelector = (function() { + function TemplateSelector(opts) { + var ref; + if (opts == null) { + opts = {}; + } + this.onClick = bind(this.onClick, this); + this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name'); + this.buildDropdown(); + this.bindEvents(); + this.onFilenameUpdate(); + } + + TemplateSelector.prototype.buildDropdown = function() { + return this.dropdown.glDropdown({ + data: this.data, + filterable: true, + selectable: true, + toggleLabel: this.toggleLabel, + search: { + fields: ['name'] + }, + clicked: this.onClick, + text: function(item) { + return item.name; + } + }); + }; + + TemplateSelector.prototype.bindEvents = function() { + return this.$input.on('keyup blur', (function(_this) { + return function(e) { + return _this.onFilenameUpdate(); + }; + })(this)); + }; + + TemplateSelector.prototype.toggleLabel = function(item) { + return item.name; + }; + + TemplateSelector.prototype.onFilenameUpdate = function() { + var filenameMatches; + if (!this.$input.length) { + return; + } + filenameMatches = this.pattern.test(this.$input.val().trim()); + if (!filenameMatches) { + this.wrapper.addClass('hidden'); + return; + } + return this.wrapper.removeClass('hidden'); + }; + + TemplateSelector.prototype.onClick = function(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + }; + + TemplateSelector.prototype.requestFile = function(item) {}; + + TemplateSelector.prototype.requestFileSuccess = function(file) { + this.editor.setValue(file.content, 1); + return this.editor.focus(); + }; + + return TemplateSelector; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee deleted file mode 100644 index 40c9169beac..00000000000 --- a/app/assets/javascripts/blob/template_selector.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -class @TemplateSelector - constructor: (opts = {}) -> - { - @dropdown, - @data, - @pattern, - @wrapper, - @editor, - @fileEndpoint, - @$input = $('#file_name') - } = opts - - @buildDropdown() - @bindEvents() - @onFilenameUpdate() - - buildDropdown: -> - @dropdown.glDropdown( - data: @data, - filterable: true, - selectable: true, - toggleLabel: @toggleLabel, - search: - fields: ['name'] - clicked: @onClick - text: (item) -> - item.name - ) - - bindEvents: -> - @$input.on('keyup blur', (e) => - @onFilenameUpdate() - ) - - toggleLabel: (item) -> - item.name - - onFilenameUpdate: -> - return unless @$input.length - - filenameMatches = @pattern.test(@$input.val().trim()) - - if not filenameMatches - @wrapper.addClass('hidden') - return - - @wrapper.removeClass('hidden') - - onClick: (item, el, e) => - e.preventDefault() - @requestFile(item) - - requestFile: (item) -> - # To be implemented on the extending class - # e.g. - # Api.gitignoreText item.name, @requestFileSuccess.bind(@) - - requestFileSuccess: (file) -> - @editor.setValue(file.content, 1) - @editor.focus() diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee deleted file mode 100644 index 5457430f921..00000000000 --- a/app/assets/javascripts/breakpoints.coffee +++ /dev/null @@ -1,37 +0,0 @@ -class @Breakpoints - instance = null; - - class BreakpointInstance - BREAKPOINTS = ["xs", "sm", "md", "lg"] - - constructor: -> - @setup() - - setup: -> - allDeviceSelector = BREAKPOINTS.map (breakpoint) -> - ".device-#{breakpoint}" - return if $(allDeviceSelector.join(",")).length - - # Create all the elements - els = $.map BREAKPOINTS, (breakpoint) -> - "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>" - $("body").append els.join('') - - visibleDevice: -> - allDeviceSelector = BREAKPOINTS.map (breakpoint) -> - ".device-#{breakpoint}" - $(allDeviceSelector.join(",")).filter(":visible") - - getBreakpointSize: -> - $visibleDevice = @visibleDevice - # the page refreshed via turbolinks - if not $visibleDevice().length - @setup() - $visibleDevice = @visibleDevice() - return $visibleDevice.attr("class").split("visible-")[1] - - @get: -> - return instance ?= new BreakpointInstance - -$ => - @bp = Breakpoints.get() diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js new file mode 100644 index 00000000000..1e0148e5798 --- /dev/null +++ b/app/assets/javascripts/breakpoints.js @@ -0,0 +1,68 @@ +(function() { + this.Breakpoints = (function() { + var BreakpointInstance, instance; + + function Breakpoints() {} + + instance = null; + + BreakpointInstance = (function() { + var BREAKPOINTS; + + BREAKPOINTS = ["xs", "sm", "md", "lg"]; + + function BreakpointInstance() { + this.setup(); + } + + BreakpointInstance.prototype.setup = function() { + var allDeviceSelector, els; + allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { + return ".device-" + breakpoint; + }); + if ($(allDeviceSelector.join(",")).length) { + return; + } + els = $.map(BREAKPOINTS, function(breakpoint) { + return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; + }); + return $("body").append(els.join('')); + }; + + BreakpointInstance.prototype.visibleDevice = function() { + var allDeviceSelector; + allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { + return ".device-" + breakpoint; + }); + return $(allDeviceSelector.join(",")).filter(":visible"); + }; + + BreakpointInstance.prototype.getBreakpointSize = function() { + var $visibleDevice; + $visibleDevice = this.visibleDevice; + if (!$visibleDevice().length) { + this.setup(); + } + $visibleDevice = this.visibleDevice(); + return $visibleDevice.attr("class").split("visible-")[1]; + }; + + return BreakpointInstance; + + })(); + + Breakpoints.get = function() { + return instance != null ? instance : instance = new BreakpointInstance; + }; + + return Breakpoints; + + })(); + + $((function(_this) { + return function() { + return _this.bp = Breakpoints.get(); + }; + })(this)); + +}).call(this); diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js new file mode 100644 index 00000000000..fceeff36728 --- /dev/null +++ b/app/assets/javascripts/broadcast_message.js @@ -0,0 +1,34 @@ +(function() { + $(function() { + var previewPath; + $('input#broadcast_message_color').on('input', function() { + var previewColor; + previewColor = $(this).val(); + return $('div.broadcast-message-preview').css('background-color', previewColor); + }); + $('input#broadcast_message_font').on('input', function() { + var previewColor; + previewColor = $(this).val(); + return $('div.broadcast-message-preview').css('color', previewColor); + }); + previewPath = $('textarea#broadcast_message_message').data('preview-path'); + return $('textarea#broadcast_message_message').on('input', function() { + var message; + message = $(this).val(); + if (message === '') { + return $('.js-broadcast-message-preview').text("Your message here"); + } else { + return $.ajax({ + url: previewPath, + type: "POST", + data: { + broadcast_message: { + message: message + } + } + }); + } + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee deleted file mode 100644 index a38a329c4c2..00000000000 --- a/app/assets/javascripts/broadcast_message.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -$ -> - $('input#broadcast_message_color').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('background-color', previewColor) - - $('input#broadcast_message_font').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('color', previewColor) - - previewPath = $('textarea#broadcast_message_message').data('preview-path') - - $('textarea#broadcast_message_message').on 'input', -> - message = $(@).val() - - if message == '' - $('.js-broadcast-message-preview').text("Your message here") - else - $.ajax( - url: previewPath - type: "POST" - data: { broadcast_message: { message: message } } - ) diff --git a/app/assets/javascripts/build.coffee b/app/assets/javascripts/build.coffee deleted file mode 100644 index cf203ea43a0..00000000000 --- a/app/assets/javascripts/build.coffee +++ /dev/null @@ -1,114 +0,0 @@ -class @Build - @interval: null - @state: null - - constructor: (@page_url, @build_url, @build_status, @state) -> - clearInterval(Build.interval) - - # Init breakpoint checker - @bp = Breakpoints.get() - @hideSidebar() - $('.js-build-sidebar').niceScroll() - $(document) - .off 'click', '.js-sidebar-build-toggle' - .on 'click', '.js-sidebar-build-toggle', @toggleSidebar - - $(window) - .off 'resize.build' - .on 'resize.build', @hideSidebar - - @updateArtifactRemoveDate() - - if $('#build-trace').length - @getInitialBuildTrace() - @initScrollButtonAffix() - - if @build_status is "running" or @build_status is "pending" - # - # Bind autoscroll button to follow build output - # - $('#autoscroll-button').on 'click', -> - state = $(this).data("state") - if "enabled" is state - $(this).data "state", "disabled" - $(this).text "enable autoscroll" - else - $(this).data "state", "enabled" - $(this).text "disable autoscroll" - - # - # Check for new build output if user still watching build page - # Only valid for runnig build when output changes during time - # - Build.interval = setInterval => - if window.location.href.split("#").first() is @page_url - @getBuildTrace() - , 4000 - - getInitialBuildTrace: -> - $.ajax - url: @build_url - dataType: 'json' - success: (build_data) -> - $('.js-build-output').html build_data.trace_html - - if build_data.status is 'success' or build_data.status is 'failed' - $('.js-build-refresh').remove() - - getBuildTrace: -> - $.ajax - url: "#{@page_url}/trace.json?state=#{encodeURIComponent(@state)}" - dataType: "json" - success: (log) => - if log.state - @state = log.state - - if log.status is "running" - if log.append - $('.js-build-output').append log.html - else - $('.js-build-output').html log.html - @checkAutoscroll() - else if log.status isnt @build_status - Turbolinks.visit @page_url - - checkAutoscroll: -> - $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") - - initScrollButtonAffix: -> - $buildScroll = $('#js-build-scroll') - $body = $('body') - $buildTrace = $('#build-trace') - - $buildScroll.affix( - offset: - bottom: -> - $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top) - ) - - shouldHideSidebar: -> - bootstrapBreakpoint = @bp.getBreakpointSize() - - bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm' - - toggleSidebar: => - if @shouldHideSidebar() - $('.js-build-sidebar') - .toggleClass 'right-sidebar-expanded right-sidebar-collapsed' - - hideSidebar: => - if @shouldHideSidebar() - $('.js-build-sidebar') - .removeClass 'right-sidebar-expanded' - .addClass 'right-sidebar-collapsed' - else - $('.js-build-sidebar') - .removeClass 'right-sidebar-collapsed' - .addClass 'right-sidebar-expanded' - - updateArtifactRemoveDate: -> - $date = $('.js-artifacts-remove') - - if $date.length - date = $date.text() - $date.text $.timefor(new Date(date), ' ') diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js new file mode 100644 index 00000000000..e135cb92a30 --- /dev/null +++ b/app/assets/javascripts/build.js @@ -0,0 +1,139 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Build = (function() { + Build.interval = null; + + Build.state = null; + + function Build(page_url, build_url, build_status, state1) { + this.page_url = page_url; + this.build_url = build_url; + this.build_status = build_status; + this.state = state1; + this.hideSidebar = bind(this.hideSidebar, this); + this.toggleSidebar = bind(this.toggleSidebar, this); + clearInterval(Build.interval); + this.bp = Breakpoints.get(); + this.hideSidebar(); + $('.js-build-sidebar').niceScroll(); + $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); + $(window).off('resize.build').on('resize.build', this.hideSidebar); + this.updateArtifactRemoveDate(); + if ($('#build-trace').length) { + this.getInitialBuildTrace(); + this.initScrollButtonAffix(); + } + if (this.build_status === "running" || this.build_status === "pending") { + $('#autoscroll-button').on('click', function() { + var state; + state = $(this).data("state"); + if ("enabled" === state) { + $(this).data("state", "disabled"); + return $(this).text("enable autoscroll"); + } else { + $(this).data("state", "enabled"); + return $(this).text("disable autoscroll"); + } + }); + Build.interval = setInterval((function(_this) { + return function() { + if (window.location.href.split("#").first() === _this.page_url) { + return _this.getBuildTrace(); + } + }; + })(this), 4000); + } + } + + Build.prototype.getInitialBuildTrace = function() { + return $.ajax({ + url: this.build_url, + dataType: 'json', + success: function(build_data) { + $('.js-build-output').html(build_data.trace_html); + if (build_data.status === 'success' || build_data.status === 'failed') { + return $('.js-build-refresh').remove(); + } + } + }); + }; + + Build.prototype.getBuildTrace = function() { + return $.ajax({ + url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)), + dataType: "json", + success: (function(_this) { + return function(log) { + if (log.state) { + _this.state = log.state; + } + if (log.status === "running") { + if (log.append) { + $('.js-build-output').append(log.html); + } else { + $('.js-build-output').html(log.html); + } + return _this.checkAutoscroll(); + } else if (log.status !== _this.build_status) { + return Turbolinks.visit(_this.page_url); + } + }; + })(this) + }); + }; + + Build.prototype.checkAutoscroll = function() { + if ("enabled" === $("#autoscroll-button").data("state")) { + return $("html,body").scrollTop($("#build-trace").height()); + } + }; + + Build.prototype.initScrollButtonAffix = function() { + var $body, $buildScroll, $buildTrace; + $buildScroll = $('#js-build-scroll'); + $body = $('body'); + $buildTrace = $('#build-trace'); + return $buildScroll.affix({ + offset: { + bottom: function() { + return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); + } + } + }); + }; + + Build.prototype.shouldHideSidebar = function() { + var bootstrapBreakpoint; + bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + }; + + Build.prototype.toggleSidebar = function() { + if (this.shouldHideSidebar()) { + return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed'); + } + }; + + Build.prototype.hideSidebar = function() { + if (this.shouldHideSidebar()) { + return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + } else { + return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + } + }; + + Build.prototype.updateArtifactRemoveDate = function() { + var $date, date; + $date = $('.js-artifacts-remove'); + if ($date.length) { + date = $date.text(); + return $date.text($.timefor(new Date(date), ' ')); + } + }; + + return Build; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js new file mode 100644 index 00000000000..f345ba0abe6 --- /dev/null +++ b/app/assets/javascripts/build_artifacts.js @@ -0,0 +1,27 @@ +(function() { + this.BuildArtifacts = (function() { + function BuildArtifacts() { + this.disablePropagation(); + this.setupEntryClick(); + } + + BuildArtifacts.prototype.disablePropagation = function() { + $('.top-block').on('click', '.download', function(e) { + return e.stopPropagation(); + }); + return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { + return e.stopImmediatePropagation(); + }); + }; + + BuildArtifacts.prototype.setupEntryClick = function() { + return $('.tree-holder').on('click', 'tr[data-link]', function(e) { + return window.location = this.dataset.link; + }); + }; + + return BuildArtifacts; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/build_artifacts.js.coffee b/app/assets/javascripts/build_artifacts.js.coffee deleted file mode 100644 index 5ae6cba56c8..00000000000 --- a/app/assets/javascripts/build_artifacts.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -class @BuildArtifacts - constructor: () -> - @disablePropagation() - @setupEntryClick() - - disablePropagation: -> - $('.top-block').on 'click', '.download', (e) -> - e.stopPropagation() - $('.tree-holder').on 'click', 'tr[data-link] a', (e) -> - e.stopImmediatePropagation() - - setupEntryClick: -> - $('.tree-holder').on 'click', 'tr[data-link]', (e) -> - window.location = @dataset.link diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js new file mode 100644 index 00000000000..23cf5b519f4 --- /dev/null +++ b/app/assets/javascripts/commit.js @@ -0,0 +1,13 @@ +(function() { + this.Commit = (function() { + function Commit() { + $('.files .diff-file').each(function() { + return new CommitFile(this); + }); + } + + return Commit; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commit.js.coffee b/app/assets/javascripts/commit.js.coffee deleted file mode 100644 index 0566e239191..00000000000 --- a/app/assets/javascripts/commit.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @Commit - constructor: -> - $('.files .diff-file').each -> - new CommitFile(this) diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js new file mode 100644 index 00000000000..be24ee56aad --- /dev/null +++ b/app/assets/javascripts/commit/file.js @@ -0,0 +1,13 @@ +(function() { + this.CommitFile = (function() { + function CommitFile(file) { + if ($('.image', file).length) { + new ImageFile(file); + } + } + + return CommitFile; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commit/file.js.coffee b/app/assets/javascripts/commit/file.js.coffee deleted file mode 100644 index 83e793863b6..00000000000 --- a/app/assets/javascripts/commit/file.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class @CommitFile - - constructor: (file) -> - if $('.image', file).length - new ImageFile(file) diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js new file mode 100644 index 00000000000..c0d0b2d049f --- /dev/null +++ b/app/assets/javascripts/commit/image-file.js @@ -0,0 +1,175 @@ +(function() { + this.ImageFile = (function() { + var prepareFrames; + + ImageFile.availWidth = 900; + + ImageFile.viewModes = ['two-up', 'swipe']; + + function ImageFile(file) { + this.file = file; + this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { + return function(deletedWidth, deletedHeight) { + return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { + if (width === deletedWidth && height === deletedHeight) { + return _this.initViewModes(); + } else { + return _this.initView('two-up'); + } + }); + }; + })(this)); + } + + ImageFile.prototype.initViewModes = function() { + var viewMode; + viewMode = ImageFile.viewModes[0]; + $('.view-modes', this.file).removeClass('hide'); + $('.view-modes-menu', this.file).on('click', 'li', (function(_this) { + return function(event) { + if (!$(event.currentTarget).hasClass('active')) { + return _this.activateViewMode(event.currentTarget.className); + } + }; + })(this)); + return this.activateViewMode(viewMode); + }; + + ImageFile.prototype.activateViewMode = function(viewMode) { + $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active'); + return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) { + return function() { + $(".view." + viewMode, _this.file).fadeIn(200); + return _this.initView(viewMode); + }; + })(this)); + }; + + ImageFile.prototype.initView = function(viewMode) { + return this.views[viewMode].call(this); + }; + + prepareFrames = function(view) { + var maxHeight, maxWidth; + maxWidth = 0; + maxHeight = 0; + $('.frame', view).each((function(_this) { + return function(index, frame) { + var height, width; + width = $(frame).width(); + height = $(frame).height(); + maxWidth = width > maxWidth ? width : maxWidth; + return maxHeight = height > maxHeight ? height : maxHeight; + }; + })(this)).css({ + width: maxWidth, + height: maxHeight + }); + return [maxWidth, maxHeight]; + }; + + ImageFile.prototype.views = { + 'two-up': function() { + return $('.two-up.view .wrap', this.file).each((function(_this) { + return function(index, wrap) { + $('img', wrap).each(function() { + var currentWidth; + currentWidth = $(this).width(); + if (currentWidth > ImageFile.availWidth / 2) { + return $(this).width(ImageFile.availWidth / 2); + } + }); + return _this.requestImageInfo($('img', wrap), function(width, height) { + $('.image-info .meta-width', wrap).text(width + "px"); + $('.image-info .meta-height', wrap).text(height + "px"); + return $('.image-info', wrap).removeClass('hide'); + }); + }; + })(this)); + }, + 'swipe': function() { + var maxHeight, maxWidth; + maxWidth = 0; + maxHeight = 0; + return $('.swipe.view', this.file).each((function(_this) { + return function(index, view) { + var ref; + ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + $('.swipe-frame', view).css({ + width: maxWidth + 16, + height: maxHeight + 28 + }); + $('.swipe-wrap', view).css({ + width: maxWidth + 1, + height: maxHeight + 2 + }); + return $('.swipe-bar', view).css({ + left: 0 + }).draggable({ + axis: 'x', + containment: 'parent', + drag: function(event) { + return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + }, + stop: function(event) { + return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + } + }); + }; + })(this)); + }, + 'onion-skin': function() { + var dragTrackWidth, maxHeight, maxWidth; + maxWidth = 0; + maxHeight = 0; + dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); + return $('.onion-skin.view', this.file).each((function(_this) { + return function(index, view) { + var ref; + ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + $('.onion-skin-frame', view).css({ + width: maxWidth + 16, + height: maxHeight + 28 + }); + $('.swipe-wrap', view).css({ + width: maxWidth + 1, + height: maxHeight + 2 + }); + return $('.dragger', view).css({ + left: dragTrackWidth + }).draggable({ + axis: 'x', + containment: 'parent', + drag: function(event) { + return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + }, + stop: function(event) { + return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + } + }); + }; + })(this)); + } + }; + + ImageFile.prototype.requestImageInfo = function(img, callback) { + var domImg; + domImg = img.get(0); + if (domImg) { + if (domImg.complete) { + return callback.call(this, domImg.naturalWidth, domImg.naturalHeight); + } else { + return img.on('load', (function(_this) { + return function() { + return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight); + }; + })(this)); + } + } + }; + + return ImageFile; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee deleted file mode 100644 index 9c723f51e54..00000000000 --- a/app/assets/javascripts/commit/image-file.js.coffee +++ /dev/null @@ -1,127 +0,0 @@ -class @ImageFile - - # Width where images must fits in, for 2-up this gets divided by 2 - @availWidth = 900 - @viewModes = ['two-up', 'swipe'] - - constructor: (@file) -> - # Determine if old and new file has same dimensions, if not show 'two-up' view - this.requestImageInfo $('.two-up.view .frame.deleted img', @file), (deletedWidth, deletedHeight) => - this.requestImageInfo $('.two-up.view .frame.added img', @file), (width, height) => - if width == deletedWidth && height == deletedHeight - this.initViewModes() - else - this.initView('two-up') - - initViewModes: -> - viewMode = ImageFile.viewModes[0] - - $('.view-modes', @file).removeClass 'hide' - $('.view-modes-menu', @file).on 'click', 'li', (event) => - unless $(event.currentTarget).hasClass('active') - this.activateViewMode(event.currentTarget.className) - - this.activateViewMode(viewMode) - - activateViewMode: (viewMode) -> - $('.view-modes-menu li', @file) - .removeClass('active') - .filter(".#{viewMode}").addClass 'active' - $(".view:visible:not(.#{viewMode})", @file).fadeOut 200, => - $(".view.#{viewMode}", @file).fadeIn(200) - this.initView viewMode - - initView: (viewMode) -> - this.views[viewMode].call(this) - - prepareFrames = (view) -> - maxWidth = 0 - maxHeight = 0 - $('.frame', view).each (index, frame) => - width = $(frame).width() - height = $(frame).height() - maxWidth = if width > maxWidth then width else maxWidth - maxHeight = if height > maxHeight then height else maxHeight - .css - width: maxWidth - height: maxHeight - - [maxWidth, maxHeight] - - views: - 'two-up': -> - $('.two-up.view .wrap', @file).each (index, wrap) => - $('img', wrap).each -> - currentWidth = $(this).width() - if currentWidth > ImageFile.availWidth / 2 - $(this).width ImageFile.availWidth / 2 - - this.requestImageInfo $('img', wrap), (width, height) -> - $('.image-info .meta-width', wrap).text "#{width}px" - $('.image-info .meta-height', wrap).text "#{height}px" - $('.image-info', wrap).removeClass('hide') - - 'swipe': -> - maxWidth = 0 - maxHeight = 0 - - $('.swipe.view', @file).each (index, view) => - - [maxWidth, maxHeight] = prepareFrames(view) - - $('.swipe-frame', view).css - width: maxWidth + 16 - height: maxHeight + 28 - - $('.swipe-wrap', view).css - width: maxWidth + 1 - height: maxHeight + 2 - - $('.swipe-bar', view).css - left: 0 - .draggable - axis: 'x' - containment: 'parent' - drag: (event) -> - $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left - stop: (event) -> - $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left - - 'onion-skin': -> - maxWidth = 0 - maxHeight = 0 - - dragTrackWidth = $('.drag-track', @file).width() - $('.dragger', @file).width() - - $('.onion-skin.view', @file).each (index, view) => - - [maxWidth, maxHeight] = prepareFrames(view) - - $('.onion-skin-frame', view).css - width: maxWidth + 16 - height: maxHeight + 28 - - $('.swipe-wrap', view).css - width: maxWidth + 1 - height: maxHeight + 2 - - $('.dragger', view).css - left: dragTrackWidth - .draggable - axis: 'x' - containment: 'parent' - drag: (event) -> - $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth) - stop: (event) -> - $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth) - - - - requestImageInfo: (img, callback) -> - domImg = img.get(0) - if domImg - if domImg.complete - callback.call(this, domImg.naturalWidth, domImg.naturalHeight) - else - img.on 'load', => - callback.call(this, domImg.naturalWidth, domImg.naturalHeight) diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js new file mode 100644 index 00000000000..37f168c5190 --- /dev/null +++ b/app/assets/javascripts/commits.js @@ -0,0 +1,58 @@ +(function() { + this.CommitsList = (function() { + function CommitsList() {} + + CommitsList.timer = null; + + CommitsList.init = function(limit) { + $("body").on("click", ".day-commits-table li.commit", function(event) { + if (event.target.nodeName !== "A") { + location.href = $(this).attr("url"); + e.stopPropagation(); + return false; + } + }); + Pager.init(limit, false); + this.content = $("#commits-list"); + this.searchField = $("#commits-search"); + return this.initSearch(); + }; + + CommitsList.initSearch = function() { + this.timer = null; + return this.searchField.keyup((function(_this) { + return function() { + clearTimeout(_this.timer); + return _this.timer = setTimeout(_this.filterResults, 500); + }; + })(this)); + }; + + CommitsList.filterResults = function() { + var commitsUrl, form, search; + form = $(".commits-search-form"); + search = CommitsList.searchField.val(); + commitsUrl = form.attr("action") + '?' + form.serialize(); + CommitsList.content.fadeTo('fast', 0.5); + return $.ajax({ + type: "GET", + url: form.attr("action"), + data: form.serialize(), + complete: function() { + return CommitsList.content.fadeTo('fast', 1.0); + }, + success: function(data) { + CommitsList.content.html(data.html); + return history.replaceState({ + page: commitsUrl + }, document.title, commitsUrl); + }, + dataType: "json" + }); + }; + + return CommitsList; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee deleted file mode 100644 index 0acb4c1955e..00000000000 --- a/app/assets/javascripts/commits.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -class @CommitsList - @timer = null - - @init: (limit) -> - $("body").on "click", ".day-commits-table li.commit", (event) -> - if event.target.nodeName != "A" - location.href = $(this).attr("url") - e.stopPropagation() - return false - - Pager.init limit, false - - @content = $("#commits-list") - @searchField = $("#commits-search") - @initSearch() - - @initSearch: -> - @timer = null - @searchField.keyup => - clearTimeout(@timer) - @timer = setTimeout(@filterResults, 500) - - @filterResults: => - form = $(".commits-search-form") - search = @searchField.val() - commitsUrl = form.attr("action") + '?' + form.serialize() - @content.fadeTo('fast', 0.5) - - $.ajax - type: "GET" - url: form.attr("action") - data: form.serialize() - complete: => - @content.fadeTo('fast', 1.0) - success: (data) => - @content.html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: commitsUrl}, document.title, commitsUrl - dataType: "json" diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js new file mode 100644 index 00000000000..342ac0e8e69 --- /dev/null +++ b/app/assets/javascripts/compare.js @@ -0,0 +1,91 @@ +(function() { + this.Compare = (function() { + function Compare(opts) { + this.opts = opts; + this.source_loading = $(".js-source-loading"); + this.target_loading = $(".js-target-loading"); + $('.js-compare-dropdown').each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return $dropdown.glDropdown({ + selectable: true, + fieldName: $dropdown.data('field-name'), + filterable: true, + id: function(obj, $el) { + return $el.data('id'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(e, el) { + if ($dropdown.is('.js-target-branch')) { + return _this.getTargetHtml(); + } else if ($dropdown.is('.js-source-branch')) { + return _this.getSourceHtml(); + } else if ($dropdown.is('.js-target-project')) { + return _this.getTargetProject(); + } + } + }); + }; + })(this)); + this.initialState(); + } + + Compare.prototype.initialState = function() { + this.getSourceHtml(); + return this.getTargetHtml(); + }; + + Compare.prototype.getTargetProject = function() { + return $.ajax({ + url: this.opts.targetProjectUrl, + data: { + target_project_id: $("input[name='merge_request[target_project_id]']").val() + }, + beforeSend: function() { + return $('.mr_target_commit').empty(); + }, + success: function(html) { + return $('.js-target-branch-dropdown .dropdown-content').html(html); + } + }); + }; + + Compare.prototype.getSourceHtml = function() { + return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { + ref: $("input[name='merge_request[source_branch]']").val() + }); + }; + + Compare.prototype.getTargetHtml = function() { + return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), + ref: $("input[name='merge_request[target_branch]']").val() + }); + }; + + Compare.prototype.sendAjax = function(url, loading, target, data) { + var $target; + $target = $(target); + return $.ajax({ + url: url, + data: data, + beforeSend: function() { + loading.show(); + return $target.empty(); + }, + success: function(html) { + loading.hide(); + $target.html(html); + return $('.js-timeago', $target).timeago(); + } + }); + }; + + return Compare; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/compare.js.coffee b/app/assets/javascripts/compare.js.coffee deleted file mode 100644 index f20992ead3e..00000000000 --- a/app/assets/javascripts/compare.js.coffee +++ /dev/null @@ -1,67 +0,0 @@ -class @Compare - constructor: (@opts) -> - @source_loading = $ ".js-source-loading" - @target_loading = $ ".js-target-loading" - - $('.js-compare-dropdown').each (i, dropdown) => - $dropdown = $(dropdown) - - $dropdown.glDropdown( - selectable: true - fieldName: $dropdown.data 'field-name' - filterable: true - id: (obj, $el) -> - $el.data 'id' - toggleLabel: (obj, $el) -> - $el.text().trim() - clicked: (e, el) => - if $dropdown.is '.js-target-branch' - @getTargetHtml() - else if $dropdown.is '.js-source-branch' - @getSourceHtml() - else if $dropdown.is '.js-target-project' - @getTargetProject() - ) - - @initialState() - - initialState: -> - @getSourceHtml() - @getTargetHtml() - - getTargetProject: -> - $.ajax( - url: @opts.targetProjectUrl - data: - target_project_id: $("input[name='merge_request[target_project_id]']").val() - beforeSend: -> - $('.mr_target_commit').empty() - success: (html) -> - $('.js-target-branch-dropdown .dropdown-content').html html - ) - - getSourceHtml: -> - @sendAjax(@opts.sourceBranchUrl, @source_loading, '.mr_source_commit', - ref: $("input[name='merge_request[source_branch]']").val() - ) - - getTargetHtml: -> - @sendAjax(@opts.targetBranchUrl, @target_loading, '.mr_target_commit', - target_project_id: $("input[name='merge_request[target_project_id]']").val() - ref: $("input[name='merge_request[target_branch]']").val() - ) - - sendAjax: (url, loading, target, data) -> - $target = $(target) - - $.ajax( - url: url - data: data - beforeSend: -> - loading.show() - $target.empty() - success: (html) -> - loading.hide() - $target.html html - $('.js-timeago', $target).timeago() - ) diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js new file mode 100644 index 00000000000..4e3a28cd163 --- /dev/null +++ b/app/assets/javascripts/compare_autocomplete.js @@ -0,0 +1,51 @@ +(function() { + this.CompareAutocomplete = (function() { + function CompareAutocomplete() { + this.initDropdown(); + } + + CompareAutocomplete.prototype.initDropdown = function() { + return $('.js-compare-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref') + } + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterByText: true, + fieldName: $dropdown.attr('name'), + filterInput: 'input[type="text"]', + renderRow: function(ref) { + var link; + if (ref.header != null) { + return $('<li />').addClass('dropdown-header').text(ref.header); + } else { + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('<li />').append(link); + } + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + } + }); + }); + }; + + return CompareAutocomplete; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/compare_autocomplete.js.coffee b/app/assets/javascripts/compare_autocomplete.js.coffee deleted file mode 100644 index 7ad9fd97637..00000000000 --- a/app/assets/javascripts/compare_autocomplete.js.coffee +++ /dev/null @@ -1,41 +0,0 @@ -class @CompareAutocomplete - constructor: -> - @initDropdown() - - initDropdown: -> - $('.js-compare-dropdown').each -> - $dropdown = $(@) - selected = $dropdown.data('selected') - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: $dropdown.data('refs-url') - data: - ref: $dropdown.data('ref') - ).done (refs) -> - callback(refs) - selectable: true - filterable: true - filterByText: true - fieldName: $dropdown.attr('name') - filterInput: 'input[type="text"]' - renderRow: (ref) -> - if ref.header? - $('<li />') - .addClass('dropdown-header') - .text(ref.header) - else - link = $('<a />') - .attr('href', '#') - .addClass(if ref is selected then 'is-active' else '') - .text(ref) - .attr('data-ref', escape(ref)) - - $('<li />') - .append(link) - id: (obj, $el) -> - $el.attr('data-ref') - toggleLabel: (obj, $el) -> - $el.text().trim() - ) diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js new file mode 100644 index 00000000000..708ab08ffac --- /dev/null +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -0,0 +1,32 @@ +(function() { + this.ConfirmDangerModal = (function() { + function ConfirmDangerModal(form, text) { + var project_path, submit; + this.form = form; + $('.js-confirm-text').text(text || ''); + $('.js-confirm-danger-input').val(''); + $('#modal-confirm-danger').modal('show'); + project_path = $('.js-confirm-danger-match').text(); + submit = $('.js-confirm-danger-submit'); + submit.disable(); + $('.js-confirm-danger-input').off('input'); + $('.js-confirm-danger-input').on('input', function() { + if (rstrip($(this).val()) === project_path) { + return submit.enable(); + } else { + return submit.disable(); + } + }); + $('.js-confirm-danger-submit').off('click'); + $('.js-confirm-danger-submit').on('click', (function(_this) { + return function() { + return _this.form.submit(); + }; + })(this)); + } + + return ConfirmDangerModal; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee deleted file mode 100644 index 66e34dd4a08..00000000000 --- a/app/assets/javascripts/confirm_danger_modal.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @ConfirmDangerModal - constructor: (form, text) -> - @form = form - $('.js-confirm-text').text(text || '') - $('.js-confirm-danger-input').val('') - $('#modal-confirm-danger').modal('show') - project_path = $('.js-confirm-danger-match').text() - submit = $('.js-confirm-danger-submit') - submit.disable() - - $('.js-confirm-danger-input').off 'input' - $('.js-confirm-danger-input').on 'input', -> - if rstrip($(@).val()) is project_path - submit.enable() - else - submit.disable() - - $('.js-confirm-danger-submit').off 'click' - $('.js-confirm-danger-submit').on 'click', => - @form.submit() diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js new file mode 100644 index 00000000000..c82798cc6a5 --- /dev/null +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -0,0 +1,42 @@ + +/*= require clipboard */ + +(function() { + var genericError, genericSuccess, showTooltip; + + genericSuccess = function(e) { + showTooltip(e.trigger, 'Copied!'); + e.clearSelection(); + return $(e.trigger).blur(); + }; + + genericError = function(e) { + var key; + if (/Mac/i.test(navigator.userAgent)) { + key = '⌘'; + } else { + key = 'Ctrl'; + } + return showTooltip(e.trigger, "Press " + key + "-C to copy"); + }; + + showTooltip = function(target, title) { + return $(target).tooltip({ + container: 'body', + html: 'true', + placement: 'auto bottom', + title: title, + trigger: 'manual' + }).tooltip('show').one('mouseleave', function() { + return $(this).tooltip('hide'); + }); + }; + + $(function() { + var clipboard; + clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); + clipboard.on('success', genericSuccess); + return clipboard.on('error', genericError); + }); + +}).call(this); diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee deleted file mode 100644 index 24301e01b10..00000000000 --- a/app/assets/javascripts/copy_to_clipboard.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -#= require clipboard - -genericSuccess = (e) -> - showTooltip(e.trigger, 'Copied!') - - # Clear the selection and blur the trigger so it loses its border - e.clearSelection() - $(e.trigger).blur() - -# Safari doesn't support `execCommand`, so instead we inform the user to -# copy manually. -# -# See http://clipboardjs.com/#browser-support -genericError = (e) -> - if /Mac/i.test(navigator.userAgent) - key = '⌘' # Command - else - key = 'Ctrl' - - showTooltip(e.trigger, "Press #{key}-C to copy") - -showTooltip = (target, title) -> - $(target). - tooltip( - container: 'body' - html: 'true' - placement: 'auto bottom' - title: title - trigger: 'manual' - ). - tooltip('show'). - one('mouseleave', -> $(this).tooltip('hide')) - -$ -> - clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]' - clipboard.on 'success', genericSuccess - clipboard.on 'error', genericError diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js new file mode 100644 index 00000000000..298f3852085 --- /dev/null +++ b/app/assets/javascripts/diff.js @@ -0,0 +1,77 @@ +(function() { + this.Diff = (function() { + var UNFOLD_COUNT; + + UNFOLD_COUNT = 20; + + function Diff() { + $('.files .diff-file').singleFileDiff(); + this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + $(document).off('click', '.js-unfold'); + $(document).on('click', '.js-unfold', (function(_this) { + return function(event) { + var line_number, link, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; + target = $(event.target); + unfoldBottom = target.hasClass('js-unfold-bottom'); + unfold = true; + ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; + offset = line_number - old_line; + if (unfoldBottom) { + line_number += 1; + since = line_number; + to = line_number + UNFOLD_COUNT; + } else { + ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; + line_number -= 1; + to = line_number; + if (line_number - UNFOLD_COUNT > prev_new_line + 1) { + since = line_number - UNFOLD_COUNT; + } else { + since = prev_new_line + 1; + unfold = false; + } + } + link = target.parents('.diff-file').attr('data-blob-diff-path'); + params = { + since: since, + to: to, + bottom: unfoldBottom, + offset: offset, + unfold: unfold, + indent: 1 + }; + return $.get(link, params, function(response) { + return target.parent().replaceWith(response); + }); + }; + })(this)); + } + + Diff.prototype.lineNumbers = function(line) { + var i, l, len, line_number, line_numbers, lines, results; + if (!line.children().length) { + return [0, 0]; + } + lines = line.children().slice(0, 2); + line_numbers = (function() { + var i, len, results; + results = []; + for (i = 0, len = lines.length; i < len; i++) { + l = lines[i]; + results.push($(l).attr('data-linenumber')); + } + return results; + })(); + results = []; + for (i = 0, len = line_numbers.length; i < len; i++) { + line_number = line_numbers[i]; + results.push(parseInt(line_number)); + } + return results; + }; + + return Diff; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee deleted file mode 100644 index c132cc8c542..00000000000 --- a/app/assets/javascripts/diff.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -class @Diff - UNFOLD_COUNT = 20 - constructor: -> - $('.files .diff-file').singleFileDiff() - @filesCommentButton = $('.files .diff-file').filesCommentButton() - - $(document).off('click', '.js-unfold') - $(document).on('click', '.js-unfold', (event) => - target = $(event.target) - unfoldBottom = target.hasClass('js-unfold-bottom') - unfold = true - - [old_line, line_number] = @lineNumbers(target.parent()) - offset = line_number - old_line - - if unfoldBottom - line_number += 1 - since = line_number - to = line_number + UNFOLD_COUNT - else - [prev_old_line, prev_new_line] = @lineNumbers(target.parent().prev()) - line_number -= 1 - to = line_number - if line_number - UNFOLD_COUNT > prev_new_line + 1 - since = line_number - UNFOLD_COUNT - else - since = prev_new_line + 1 - unfold = false - - link = target.parents('.diff-file').attr('data-blob-diff-path') - params = - since: since - to: to - 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 - - $.get(link, params, (response) -> - target.parent().replaceWith(response) - ) - ) - - lineNumbers: (line) -> - return ([0, 0]) unless line.children().length - lines = line.children().slice(0, 2) - line_numbers = ($(l).attr('data-linenumber') for l in lines) - (parseInt(line_number) for line_number in line_numbers) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js new file mode 100644 index 00000000000..d212d66da1b --- /dev/null +++ b/app/assets/javascripts/dispatcher.js @@ -0,0 +1,255 @@ +(function() { + var Dispatcher; + + $(function() { + return new Dispatcher(); + }); + + Dispatcher = (function() { + function Dispatcher() { + this.initSearch(); + this.initPageScripts(); + } + + Dispatcher.prototype.initPageScripts = function() { + var page, path, shortcut_handler; + page = $('body').attr('data-page'); + if (!page) { + return false; + } + path = page.split(':'); + shortcut_handler = null; + switch (page) { + case 'projects:issues:index': + Issuable.init(); + new IssuableBulkActions(); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:issues:show': + new Issue(); + shortcut_handler = new ShortcutsIssuable(); + new ZenMode(); + break; + case 'projects:milestones:show': + case 'groups:milestones:show': + case 'dashboard:milestones:show': + new Milestone(); + break; + case 'dashboard:todos:index': + new Todos(); + break; + case 'projects:milestones:new': + case 'projects:milestones:edit': + new ZenMode(); + new DueDateSelect(); + new GLForm($('.milestone-form')); + break; + case 'groups:milestones:new': + new ZenMode(); + break; + case 'projects:compare:show': + new Diff(); + break; + case 'projects:issues:new': + case 'projects:issues:edit': + shortcut_handler = new ShortcutsNavigation(); + new GLForm($('.issue-form')); + new IssuableForm($('.issue-form')); + break; + case 'projects:merge_requests:new': + case 'projects:merge_requests:edit': + new Diff(); + shortcut_handler = new ShortcutsNavigation(); + new GLForm($('.merge-request-form')); + new IssuableForm($('.merge-request-form')); + break; + case 'projects:tags:new': + new ZenMode(); + new GLForm($('.tag-form')); + break; + case 'projects:releases:edit': + new ZenMode(); + new GLForm($('.release-form')); + break; + case 'projects:merge_requests:show': + new Diff(); + shortcut_handler = new ShortcutsIssuable(true); + new ZenMode(); + new MergedButtons(); + break; + case 'projects:merge_requests:commits': + case 'projects:merge_requests:builds': + new MergedButtons(); + break; + case "projects:merge_requests:diffs": + new Diff(); + new ZenMode(); + new MergedButtons(); + break; + case 'projects:merge_requests:index': + shortcut_handler = new ShortcutsNavigation(); + Issuable.init(); + break; + case 'dashboard:activity': + new Activities(); + break; + case 'dashboard:projects:starred': + new Activities(); + break; + case 'projects:commit:show': + new Commit(); + new Diff(); + new ZenMode(); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:commits:show': + case 'projects:activity': + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + if ($('#tree-slider').length) { + new TreeView(); + } + break; + case 'groups:activity': + new Activities(); + break; + case 'groups:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'groups:group_members:index': + new GroupMembers(); + new UsersSelect(); + break; + case 'projects:project_members:index': + new ProjectMembers(); + new UsersSelect(); + break; + case 'groups:new': + case 'groups:edit': + case 'admin:groups:edit': + case 'admin:groups:new': + new GroupAvatar(); + break; + case 'projects:tree:show': + shortcut_handler = new ShortcutsNavigation(); + new TreeView(); + break; + case 'projects:find_file:show': + shortcut_handler = true; + break; + case 'projects:blob:show': + case 'projects:blame:show': + new LineHighlighter(); + shortcut_handler = new ShortcutsNavigation(); + new ShortcutsBlob(true); + break; + case 'projects:labels:new': + case 'projects:labels:edit': + new Labels(); + break; + case 'projects:labels:index': + if ($('.prioritized-labels').length) { + new LabelManager(); + } + break; + case 'projects:network:show': + shortcut_handler = true; + break; + case 'projects:forks:new': + new ProjectFork(); + break; + case 'projects:artifacts:browse': + new BuildArtifacts(); + break; + case 'projects:group_links:index': + new GroupsSelect(); + break; + case 'search:show': + new Search(); + } + switch (path.first()) { + case 'admin': + new Admin(); + switch (path[1]) { + case 'groups': + new UsersSelect(); + break; + case 'projects': + new NamespaceSelects(); + } + break; + case 'dashboard': + case 'root': + shortcut_handler = new ShortcutsDashboardNavigation(); + break; + case 'profiles': + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'projects': + new Project(); + new ProjectAvatar(); + switch (path[1]) { + case 'compare': + new CompareAutocomplete(); + break; + case 'edit': + shortcut_handler = new ShortcutsNavigation(); + new ProjectNew(); + break; + case 'new': + new ProjectNew(); + break; + case 'show': + new ProjectNew(); + new ProjectShow(); + new NotificationsDropdown(); + break; + case 'wikis': + new Wikis(); + shortcut_handler = new ShortcutsNavigation(); + new ZenMode(); + new GLForm($('.wiki-form')); + break; + case 'snippets': + shortcut_handler = new ShortcutsNavigation(); + if (path[2] === 'show') { + new ZenMode(); + } + break; + case 'labels': + case 'graphs': + case 'compare': + case 'pipelines': + case 'forks': + case 'milestones': + case 'project_members': + case 'deploy_keys': + case 'builds': + case 'hooks': + case 'services': + case 'protected_branches': + shortcut_handler = new ShortcutsNavigation(); + } + } + if (!shortcut_handler) { + return new Shortcuts(); + } + }; + + Dispatcher.prototype.initSearch = function() { + if ($('.search').length) { + return new SearchAutocomplete(); + } + }; + + return Dispatcher; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee deleted file mode 100644 index afaa6407b05..00000000000 --- a/app/assets/javascripts/dispatcher.js.coffee +++ /dev/null @@ -1,171 +0,0 @@ -$ -> - new Dispatcher() - -class Dispatcher - constructor: () -> - @initSearch() - @initPageScripts() - - initPageScripts: -> - page = $('body').attr('data-page') - - unless page - return false - - path = page.split(':') - shortcut_handler = null - switch page - when 'projects:issues:index' - Issuable.init() - new IssuableBulkActions() - shortcut_handler = new ShortcutsNavigation() - when 'projects:issues:show' - new Issue() - shortcut_handler = new ShortcutsIssuable() - new ZenMode() - when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' - new Milestone() - when 'dashboard:todos:index' - new Todos() - when 'projects:milestones:new', 'projects:milestones:edit' - new ZenMode() - new DueDateSelect() - new GLForm($('.milestone-form')) - when 'groups:milestones:new' - new ZenMode() - when 'projects:compare:show' - new Diff() - when 'projects:issues:new','projects:issues:edit' - shortcut_handler = new ShortcutsNavigation() - new GLForm($('.issue-form')) - new IssuableForm($('.issue-form')) - when 'projects:merge_requests:new', 'projects:merge_requests:edit' - new Diff() - shortcut_handler = new ShortcutsNavigation() - new GLForm($('.merge-request-form')) - new IssuableForm($('.merge-request-form')) - when 'projects:tags:new' - new ZenMode() - new GLForm($('.tag-form')) - when 'projects:releases:edit' - new ZenMode() - new GLForm($('.release-form')) - when 'projects:merge_requests:show' - new Diff() - shortcut_handler = new ShortcutsIssuable(true) - new ZenMode() - new MergedButtons() - when 'projects:merge_requests:commits', 'projects:merge_requests:builds' - new MergedButtons() - when "projects:merge_requests:diffs" - new Diff() - new ZenMode() - new MergedButtons() - when 'projects:merge_requests:index' - shortcut_handler = new ShortcutsNavigation() - Issuable.init() - when 'dashboard:activity' - new Activities() - when 'dashboard:projects:starred' - new Activities() - when 'projects:commit:show' - new Commit() - new Diff() - new ZenMode() - shortcut_handler = new ShortcutsNavigation() - when 'projects:commits:show', 'projects:activity' - shortcut_handler = new ShortcutsNavigation() - when 'projects:show' - shortcut_handler = new ShortcutsNavigation() - - new NotificationsForm() - new TreeView() if $('#tree-slider').length - when 'groups:activity' - new Activities() - when 'groups:show' - shortcut_handler = new ShortcutsNavigation() - new NotificationsForm() - new NotificationsDropdown() - when 'groups:group_members:index' - new GroupMembers() - new UsersSelect() - when 'projects:project_members:index' - new ProjectMembers() - new UsersSelect() - when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new' - new GroupAvatar() - when 'projects:tree:show' - shortcut_handler = new ShortcutsNavigation() - new TreeView() - when 'projects:find_file:show' - shortcut_handler = true - when 'projects:blob:show', 'projects:blame:show' - new LineHighlighter() - shortcut_handler = new ShortcutsNavigation() - new ShortcutsBlob true - when 'projects:labels:new', 'projects:labels:edit' - new Labels() - when 'projects:labels:index' - new LabelManager() if $('.prioritized-labels').length - when '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 - when 'projects:forks:new' - new ProjectFork() - when 'projects:artifacts:browse' - new BuildArtifacts() - when 'projects:group_links:index' - new GroupsSelect() - when 'search:show' - new Search() - - switch path.first() - when 'admin' - new Admin() - switch path[1] - when 'groups' - new UsersSelect() - when 'projects' - new NamespaceSelects() - when 'dashboard', 'root' - shortcut_handler = new ShortcutsDashboardNavigation() - when 'profiles' - new NotificationsForm() - new NotificationsDropdown() - when 'projects' - new Project() - new ProjectAvatar() - switch path[1] - when 'compare' - new CompareAutocomplete() - when 'edit' - shortcut_handler = new ShortcutsNavigation() - new ProjectNew() - when 'new' - new ProjectNew() - when 'show' - new ProjectNew() - new ProjectShow() - new NotificationsDropdown() - when 'wikis' - new Wikis() - shortcut_handler = new ShortcutsNavigation() - new ZenMode() - new GLForm($('.wiki-form')) - when 'snippets' - shortcut_handler = new ShortcutsNavigation() - new ZenMode() if path[2] == 'show' - when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \ - 'milestones', 'project_members', 'deploy_keys', 'builds', \ - 'hooks', 'services', 'protected_branches' - shortcut_handler = new ShortcutsNavigation() - - # If we haven't installed a custom shortcut handler, install the default one - if not shortcut_handler - new Shortcuts() - - initSearch: -> - - # Only when search form is present - new SearchAutocomplete() if $('.search').length diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js new file mode 100644 index 00000000000..288cce04f87 --- /dev/null +++ b/app/assets/javascripts/dropzone_input.js @@ -0,0 +1,219 @@ + +/*= require markdown_preview */ + +(function() { + this.DropzoneInput = (function() { + function DropzoneInput(form) { + var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; + Dropzone.autoDiscover = false; + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; + divHover = "<div class=\"div-dropzone-hover\"></div>"; + divSpinner = "<div class=\"div-dropzone-spinner\"></div>"; + divAlert = "<div class=\"" + alertClass + "\"></div>"; + iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>"; + iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; + uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); + btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>"; + project_uploads_path = window.project_uploads_path || null; + max_file_size = gon.max_file_size || 10; + form_textarea = $(form).find(".js-gfm-input"); + form_textarea.wrap("<div class=\"div-dropzone\"></div>"); + form_textarea.on('paste', (function(_this) { + return function(event) { + return handlePaste(event); + }; + })(this)); + $mdArea = $(form_textarea).closest('.md-area'); + $(form).setupMarkdownPreview(); + form_dropzone = $(form).find('.div-dropzone'); + form_dropzone.parent().addClass("div-dropzone-wrapper"); + form_dropzone.append(divHover); + form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); + form_dropzone.append(divSpinner); + form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); + form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + dropzone = form_dropzone.dropzone({ + url: project_uploads_path, + dictDefaultMessage: "", + clickable: true, + paramName: "file", + maxFilesize: max_file_size, + uploadMultiple: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + previewContainer: false, + processing: function() { + return $(".div-dropzone-alert").alert("close"); + }, + dragover: function() { + $mdArea.addClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0.7); + }, + dragleave: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + }, + drop: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + form_textarea.focus(); + }, + success: function(header, response) { + pasteText(response.link.markdown); + }, + error: function(temp) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); + } + }, + totaluploadprogress: function(totalUploadProgress) { + uploadProgress.text(Math.round(totalUploadProgress) + "%"); + }, + sending: function() { + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }, + queuecomplete: function() { + uploadProgress.text(""); + $(".dz-preview").remove(); + $(".markdown-area").trigger("input"); + $(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + } + }); + child = $(dropzone[0]).children("textarea"); + handlePaste = function(event) { + var filename, image, pasteEvent, text; + pasteEvent = event.originalEvent; + if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { + image = isImage(pasteEvent); + if (image) { + event.preventDefault(); + filename = getFilename(pasteEvent) || "image.png"; + text = "{{" + filename + "}}"; + pasteText(text); + return uploadFile(image.getAsFile(), filename); + } + } + }; + isImage = function(data) { + var i, item; + i = 0; + while (i < data.clipboardData.items.length) { + item = data.clipboardData.items[i]; + if (item.type.indexOf("image") !== -1) { + return item; + } + i++; + } + return false; + }; + pasteText = function(text) { + var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; + caretStart = $(child)[0].selectionStart; + caretEnd = $(child)[0].selectionEnd; + textEnd = $(child).val().length; + beforeSelection = $(child).val().substring(0, caretStart); + afterSelection = $(child).val().substring(caretEnd, textEnd); + $(child).val(beforeSelection + text + afterSelection); + child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length); + return form_textarea.trigger("input"); + }; + getFilename = function(e) { + var value; + if (window.clipboardData && window.clipboardData.getData) { + value = window.clipboardData.getData("Text"); + } else if (e.clipboardData && e.clipboardData.getData) { + value = e.clipboardData.getData("text/plain"); + } + value = value.split("\r"); + return value.first(); + }; + uploadFile = function(item, filename) { + var formData; + formData = new FormData(); + formData.append("file", item, filename); + return $.ajax({ + url: project_uploads_path, + type: "POST", + data: formData, + dataType: "json", + processData: false, + contentType: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + beforeSend: function() { + showSpinner(); + return closeAlertMessage(); + }, + success: function(e, textStatus, response) { + return insertToTextArea(filename, response.responseJSON.link.markdown); + }, + error: function(response) { + return showError(response.responseJSON.message); + }, + complete: function() { + return closeSpinner(); + } + }); + }; + insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace("{{" + filename + "}}", url + "\n"); + }); + }; + appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + showSpinner = function(e) { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }; + closeSpinner = function() { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + }; + showError = function(message) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + return $(".div-dropzone-alert").append(btnAlert + message); + } + }; + closeAlertMessage = function() { + return form.find(".div-dropzone-alert").alert("close"); + }; + form.find(".markdown-selector").click(function(e) { + e.preventDefault(); + $(this).closest('.gfm-form').find('.div-dropzone').click(); + }); + } + + return DropzoneInput; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee deleted file mode 100644 index 665246e2a7d..00000000000 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ /dev/null @@ -1,201 +0,0 @@ -#= require markdown_preview - -class @DropzoneInput - constructor: (form) -> - Dropzone.autoDiscover = false - alertClass = "alert alert-danger alert-dismissable div-dropzone-alert" - alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"" - divHover = "<div class=\"div-dropzone-hover\"></div>" - divSpinner = "<div class=\"div-dropzone-spinner\"></div>" - divAlert = "<div class=\"" + alertClass + "\"></div>" - iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>" - iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" - uploadProgress = $("<div class=\"div-dropzone-progress\"></div>") - btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" - project_uploads_path = window.project_uploads_path or null - max_file_size = gon.max_file_size or 10 - - form_textarea = $(form).find(".js-gfm-input") - form_textarea.wrap "<div class=\"div-dropzone\"></div>" - form_textarea.on 'paste', (event) => - handlePaste(event) - - $mdArea = $(form_textarea).closest('.md-area') - - $(form).setupMarkdownPreview() - - form_dropzone = $(form).find('.div-dropzone') - form_dropzone.parent().addClass "div-dropzone-wrapper" - form_dropzone.append divHover - form_dropzone.find(".div-dropzone-hover").append iconPaperclip - form_dropzone.append divSpinner - form_dropzone.find(".div-dropzone-spinner").append iconSpinner - form_dropzone.find(".div-dropzone-spinner").append uploadProgress - form_dropzone.find(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - - dropzone = form_dropzone.dropzone( - url: project_uploads_path - dictDefaultMessage: "" - clickable: true - paramName: "file" - maxFilesize: max_file_size - uploadMultiple: false - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - previewContainer: false - - processing: -> - $(".div-dropzone-alert").alert "close" - - dragover: -> - $mdArea.addClass 'is-dropzone-hover' - form.find(".div-dropzone-hover").css "opacity", 0.7 - return - - dragleave: -> - $mdArea.removeClass 'is-dropzone-hover' - form.find(".div-dropzone-hover").css "opacity", 0 - return - - drop: -> - $mdArea.removeClass 'is-dropzone-hover' - form.find(".div-dropzone-hover").css "opacity", 0 - form_textarea.focus() - return - - success: (header, response) -> - pasteText response.link.markdown - return - - error: (temp) -> - errorAlert = $(form).find('.error-alert') - checkIfMsgExists = errorAlert.children().length - if checkIfMsgExists is 0 - errorAlert.append divAlert - $(".div-dropzone-alert").append "#{btnAlert}Attaching the file failed." - return - - totaluploadprogress: (totalUploadProgress) -> - uploadProgress.text Math.round(totalUploadProgress) + "%" - return - - sending: -> - form_dropzone.find(".div-dropzone-spinner").css - "opacity": 0.7 - "display": "inherit" - return - - queuecomplete: -> - uploadProgress.text "" - $(".dz-preview").remove() - $(".markdown-area").trigger "input" - $(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - return - ) - - child = $(dropzone[0]).children("textarea") - - handlePaste = (event) -> - pasteEvent = event.originalEvent - if pasteEvent.clipboardData and pasteEvent.clipboardData.items - image = isImage(pasteEvent) - if image - event.preventDefault() - - filename = getFilename(pasteEvent) or "image.png" - text = "{{" + filename + "}}" - pasteText(text) - uploadFile image.getAsFile(), filename - - isImage = (data) -> - i = 0 - while i < data.clipboardData.items.length - item = data.clipboardData.items[i] - if item.type.indexOf("image") isnt -1 - return item - i++ - return false - - pasteText = (text) -> - caretStart = $(child)[0].selectionStart - caretEnd = $(child)[0].selectionEnd - textEnd = $(child).val().length - - beforeSelection = $(child).val().substring 0, caretStart - afterSelection = $(child).val().substring caretEnd, textEnd - $(child).val beforeSelection + text + afterSelection - child.get(0).setSelectionRange caretStart + text.length, caretEnd + text.length - form_textarea.trigger "input" - - getFilename = (e) -> - if window.clipboardData and window.clipboardData.getData - value = window.clipboardData.getData("Text") - else if e.clipboardData and e.clipboardData.getData - value = e.clipboardData.getData("text/plain") - - value = value.split("\r") - value.first() - - uploadFile = (item, filename) -> - formData = new FormData() - formData.append "file", item, filename - $.ajax - url: project_uploads_path - type: "POST" - data: formData - dataType: "json" - processData: false - contentType: false - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - beforeSend: -> - showSpinner() - closeAlertMessage() - - success: (e, textStatus, response) -> - insertToTextArea(filename, response.responseJSON.link.markdown) - - error: (response) -> - showError(response.responseJSON.message) - - complete: -> - closeSpinner() - - insertToTextArea = (filename, url) -> - $(child).val (index, val) -> - val.replace("{{" + filename + "}}", url + "\n") - - appendToTextArea = (url) -> - $(child).val (index, val) -> - val + url + "\n" - - showSpinner = (e) -> - form.find(".div-dropzone-spinner").css - "opacity": 0.7 - "display": "inherit" - - closeSpinner = -> - form.find(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - - showError = (message) -> - errorAlert = $(form).find('.error-alert') - checkIfMsgExists = errorAlert.children().length - if checkIfMsgExists is 0 - errorAlert.append divAlert - $(".div-dropzone-alert").append btnAlert + message - - closeAlertMessage = -> - form.find(".div-dropzone-alert").alert "close" - - form.find(".markdown-selector").click (e) -> - e.preventDefault() - $(@).closest('.gfm-form').find('.div-dropzone').click() - return diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js new file mode 100644 index 00000000000..5a725a41fd1 --- /dev/null +++ b/app/assets/javascripts/due_date_select.js @@ -0,0 +1,104 @@ +(function() { + this.DueDateSelect = (function() { + function DueDateSelect() { + var $datePicker, $dueDate, $loading; + $datePicker = $('.datepicker'); + if ($datePicker.length) { + $dueDate = $('#milestone_due_date'); + $datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + onSelect: function(dateText, inst) { + return $dueDate.val(dateText); + } + }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())); + } + $('.js-clear-due-date').on('click', function(e) { + e.preventDefault(); + return $.datepicker._clearDate($datePicker); + }); + $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; + $dropdown = $(dropdown); + $dropdownParent = $dropdown.closest('.dropdown'); + $datePicker = $dropdownParent.find('.js-due-date-calendar'); + $block = $dropdown.closest('.block'); + $selectbox = $dropdown.closest('.selectbox'); + $value = $block.find('.value'); + $valueContent = $block.find('.value-content'); + $sidebarValue = $('.js-due-date-sidebar-value', $block); + fieldName = $dropdown.data('field-name'); + abilityName = $dropdown.data('ability-name'); + issueUpdateURL = $dropdown.data('issue-update'); + $dropdown.glDropdown({ + hidden: function() { + $selectbox.hide(); + return $value.css('display', ''); + } + }); + addDueDate = function(isDropdown) { + var data, date, mediumDate, value; + value = $("input[name='" + fieldName + "']").val(); + if (value !== '') { + date = new Date(value.replace(new RegExp('-', 'g'), ',')); + mediumDate = $.datepicker.formatDate('M d, yy', date); + } else { + mediumDate = 'No due date'; + } + data = {}; + data[abilityName] = {}; + data[abilityName].due_date = value; + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + data: data, + dataType: 'json', + beforeSend: function() { + var cssClass; + $loading.fadeIn(); + if (isDropdown) { + $dropdown.trigger('loading.gl.dropdown'); + $selectbox.hide(); + } + $value.css('display', ''); + cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value'; + $valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>"); + $sidebarValue.html(mediumDate); + if (value !== '') { + return $('.js-remove-due-date-holder').removeClass('hidden'); + } else { + return $('.js-remove-due-date-holder').addClass('hidden'); + } + } + }).done(function(data) { + if (isDropdown) { + $dropdown.trigger('loaded.gl.dropdown'); + $dropdown.dropdown('toggle'); + } + return $loading.fadeOut(); + }); + }; + $block.on('click', '.js-remove-due-date', function(e) { + e.preventDefault(); + $("input[name='" + fieldName + "']").val(''); + return addDueDate(false); + }); + return $datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='" + fieldName + "']").val(), + altField: "input[name='" + fieldName + "']", + onSelect: function() { + return addDueDate(true); + } + }); + }); + $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) { + return e.stopImmediatePropagation(); + }); + } + + return DueDateSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee deleted file mode 100644 index d65c018dad5..00000000000 --- a/app/assets/javascripts/due_date_select.js.coffee +++ /dev/null @@ -1,99 +0,0 @@ -class @DueDateSelect - constructor: -> - # Milestone edit/new form - $datePicker = $('.datepicker') - - if $datePicker.length - $dueDate = $('#milestone_due_date') - $datePicker.datepicker - dateFormat: 'yy-mm-dd' - onSelect: (dateText, inst) -> - $dueDate.val(dateText) - .datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())) - - $('.js-clear-due-date').on 'click', (e) -> - e.preventDefault() - $.datepicker._clearDate($datePicker) - - # Issuable sidebar - $loading = $('.js-issuable-update .due_date') - .find('.block-loading') - .hide() - - $('.js-due-date-select').each (i, dropdown) -> - $dropdown = $(dropdown) - $dropdownParent = $dropdown.closest('.dropdown') - $datePicker = $dropdownParent.find('.js-due-date-calendar') - $block = $dropdown.closest('.block') - $selectbox = $dropdown.closest('.selectbox') - $value = $block.find('.value') - $valueContent = $block.find('.value-content') - $sidebarValue = $('.js-due-date-sidebar-value', $block) - - fieldName = $dropdown.data('field-name') - abilityName = $dropdown.data('ability-name') - issueUpdateURL = $dropdown.data('issue-update') - - $dropdown.glDropdown( - hidden: -> - $selectbox.hide() - $value.css('display', '') - ) - - addDueDate = (isDropdown) -> - # Create the post date - value = $("input[name='#{fieldName}']").val() - - if value isnt '' - date = new Date value.replace(new RegExp('-', 'g'), ',') - mediumDate = $.datepicker.formatDate 'M d, yy', date - else - mediumDate = 'No due date' - - data = {} - data[abilityName] = {} - data[abilityName].due_date = value - - $.ajax( - type: 'PUT' - url: issueUpdateURL - data: data - dataType: 'json' - beforeSend: -> - $loading.fadeIn() - if isDropdown - $dropdown.trigger('loading.gl.dropdown') - $selectbox.hide() - $value.css('display', '') - - cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value' - $valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>") - $sidebarValue.html(mediumDate) - - if value isnt '' - $('.js-remove-due-date-holder').removeClass 'hidden' - else - $('.js-remove-due-date-holder').addClass 'hidden' - ).done (data) -> - if isDropdown - $dropdown.trigger('loaded.gl.dropdown') - $dropdown.dropdown('toggle') - $loading.fadeOut() - - $block.on 'click', '.js-remove-due-date', (e) -> - e.preventDefault() - $("input[name='#{fieldName}']").val '' - addDueDate(false) - - $datePicker.datepicker( - dateFormat: 'yy-mm-dd', - defaultDate: $("input[name='#{fieldName}']").val() - altField: "input[name='#{fieldName}']" - onSelect: -> - addDueDate(true) - ) - - $(document) - .off 'click', '.ui-datepicker-header a' - .on 'click', '.ui-datepicker-header a', (e) -> - e.stopImmediatePropagation() diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js new file mode 100644 index 00000000000..ae3dde63da3 --- /dev/null +++ b/app/assets/javascripts/extensions/jquery.js @@ -0,0 +1,14 @@ +(function() { + $.fn.extend({ + disable: function() { + return $(this).attr('disabled', 'disabled').addClass('disabled'); + } + }); + + $.fn.extend({ + enable: function() { + return $(this).removeAttr('disabled').removeClass('disabled'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee deleted file mode 100644 index 0a9db8eb5ef..00000000000 --- a/app/assets/javascripts/extensions/jquery.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -# Disable an element and add the 'disabled' Bootstrap class -$.fn.extend disable: -> - $(@) - .attr('disabled', 'disabled') - .addClass('disabled') - -# Enable an element and remove the 'disabled' Bootstrap class -$.fn.extend enable: -> - $(@) - .removeAttr('disabled') - .removeClass('disabled') diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js new file mode 100644 index 00000000000..09b5eb398d4 --- /dev/null +++ b/app/assets/javascripts/files_comment_button.js @@ -0,0 +1,141 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.FilesCommentButton = (function() { + var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; + + COMMENT_BUTTON_CLASS = '.add-diff-note'; + + COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + + LINE_HOLDER_CLASS = '.line_holder'; + + LINE_NUMBER_CLASS = 'diff-line-num'; + + LINE_CONTENT_CLASS = 'line_content'; + + UNFOLDABLE_LINE_CLASS = 'js-unfold'; + + EMPTY_CELL_CLASS = 'empty-cell'; + + OLD_LINE_CLASS = 'old_line'; + + LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; + + TEXT_FILE_SELECTOR = '.text-file'; + + DEBOUNCE_TIMEOUT_DURATION = 100; + + function FilesCommentButton(filesContainerElement) { + var debounce; + this.filesContainerElement = filesContainerElement; + this.destroy = bind(this.destroy, this); + this.render = bind(this.render, this); + this.VIEW_TYPE = $('input#view[type=hidden]').val(); + debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); + $(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); + } + + FilesCommentButton.prototype.render = function(e) { + var $currentTarget, buttonParentElement, lineContentElement, textFileElement; + $currentTarget = $(e.currentTarget); + buttonParentElement = this.getButtonParent($currentTarget); + if (!this.shouldRender(e, buttonParentElement)) { + return; + } + textFileElement = this.getTextFileElement($currentTarget); + lineContentElement = this.getLineContent($currentTarget); + buttonParentElement.append(this.buildButton({ + noteableType: textFileElement.attr('data-noteable-type'), + noteableID: textFileElement.attr('data-noteable-id'), + commitID: textFileElement.attr('data-commit-id'), + noteType: lineContentElement.attr('data-note-type'), + position: lineContentElement.attr('data-position'), + lineType: lineContentElement.attr('data-line-type'), + discussionID: lineContentElement.attr('data-discussion-id'), + lineCode: lineContentElement.attr('data-line-code') + })); + }; + + FilesCommentButton.prototype.destroy = function(e) { + if (this.isMovingToSameType(e)) { + return; + } + $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); + }; + + FilesCommentButton.prototype.buildButton = function(buttonAttributes) { + var initializedButtonTemplate; + initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({ + COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1) + }); + return $(initializedButtonTemplate).attr({ + 'data-noteable-type': buttonAttributes.noteableType, + 'data-noteable-id': buttonAttributes.noteableID, + 'data-commit-id': buttonAttributes.commitID, + 'data-note-type': buttonAttributes.noteType, + 'data-line-code': buttonAttributes.lineCode, + 'data-position': buttonAttributes.position, + 'data-discussion-id': buttonAttributes.discussionID, + 'data-line-type': buttonAttributes.lineType + }); + }; + + FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { + return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); + }; + + FilesCommentButton.prototype.getLineContent = function(hoveredElement) { + if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + return hoveredElement; + } + if (this.VIEW_TYPE === 'inline') { + return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); + } else { + return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + } + }; + + FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { + if (this.VIEW_TYPE === 'inline') { + if (hoveredElement.hasClass(OLD_LINE_CLASS)) { + return hoveredElement; + } + return hoveredElement.parent().find("." + OLD_LINE_CLASS); + } else { + if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { + return hoveredElement; + } + return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + } + }; + + FilesCommentButton.prototype.isMovingToSameType = function(e) { + var newButtonParent; + newButtonParent = this.getButtonParent($(e.toElement)); + if (!newButtonParent) { + return false; + } + return newButtonParent.is(this.getButtonParent($(e.currentTarget))); + }; + + FilesCommentButton.prototype.shouldRender = function(e, buttonParentElement) { + return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; + }; + + return FilesCommentButton; + + })(); + + $.fn.filesCommentButton = function() { + if (!(this && (this.parent().data('can-create-note') != null))) { + return; + } + return this.each(function() { + if (!$.data(this, 'filesCommentButton')) { + return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); + } + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/files_comment_button.js.coffee b/app/assets/javascripts/files_comment_button.js.coffee deleted file mode 100644 index db0bf7082a9..00000000000 --- a/app/assets/javascripts/files_comment_button.js.coffee +++ /dev/null @@ -1,97 +0,0 @@ -class @FilesCommentButton - COMMENT_BUTTON_CLASS = '.add-diff-note' - COMMENT_BUTTON_TEMPLATE = _.template '<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>' - LINE_HOLDER_CLASS = '.line_holder' - LINE_NUMBER_CLASS = 'diff-line-num' - LINE_CONTENT_CLASS = 'line_content' - UNFOLDABLE_LINE_CLASS = 'js-unfold' - EMPTY_CELL_CLASS = 'empty-cell' - OLD_LINE_CLASS = 'old_line' - NEW_CLASS = 'new' - LINE_COLUMN_CLASSES = ".#{LINE_NUMBER_CLASS}, .line_content" - TEXT_FILE_SELECTOR = '.text-file' - DEBOUNCE_TIMEOUT_DURATION = 100 - - constructor: (@filesContainerElement) -> - @VIEW_TYPE = $('input#view[type=hidden]').val() - - debounce = _.debounce @render, DEBOUNCE_TIMEOUT_DURATION - - $(document) - .on 'mouseover', LINE_COLUMN_CLASSES, debounce - .on 'mouseleave', LINE_COLUMN_CLASSES, @destroy - - render: (e) => - $currentTarget = $(e.currentTarget) - buttonParentElement = @getButtonParent $currentTarget - return unless @shouldRender e, buttonParentElement - - textFileElement = @getTextFileElement $currentTarget - lineContentElement = @getLineContent $currentTarget - - buttonParentElement.append @buildButton - noteableType: textFileElement.attr 'data-noteable-type' - noteableID: textFileElement.attr 'data-noteable-id' - commitID: textFileElement.attr 'data-commit-id' - noteType: lineContentElement.attr 'data-note-type' - position: lineContentElement.attr 'data-position' - lineType: lineContentElement.attr 'data-line-type' - discussionID: lineContentElement.attr 'data-discussion-id' - lineCode: lineContentElement.attr 'data-line-code' - return - - destroy: (e) => - return if @isMovingToSameType e - $(COMMENT_BUTTON_CLASS, @getButtonParent $(e.currentTarget)).remove() - return - - buildButton: (buttonAttributes) -> - initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE - COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr 1 - $(initializedButtonTemplate).attr - 'data-noteable-type': buttonAttributes.noteableType - 'data-noteable-id': buttonAttributes.noteableID - 'data-commit-id': buttonAttributes.commitID - 'data-note-type': buttonAttributes.noteType - 'data-line-code': buttonAttributes.lineCode - 'data-position': buttonAttributes.position - 'data-discussion-id': buttonAttributes.discussionID - 'data-line-type': buttonAttributes.lineType - - getTextFileElement: (hoveredElement) -> - $(hoveredElement.closest TEXT_FILE_SELECTOR) - - getLineContent: (hoveredElement) -> - return hoveredElement if hoveredElement.hasClass LINE_CONTENT_CLASS - - $(".#{LINE_CONTENT_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent()) - - getButtonParent: (hoveredElement) -> - if @VIEW_TYPE is 'inline' - return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS - - $(".#{OLD_LINE_CLASS}", hoveredElement.parent()) - else - return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS - - $(".#{LINE_NUMBER_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent()) - - diffTypeClass: (hoveredElement) -> - if hoveredElement.hasClass(NEW_CLASS) then '.new' else '.old' - - isMovingToSameType: (e) -> - newButtonParent = @getButtonParent $(e.toElement) - return false unless newButtonParent - newButtonParent.is @getButtonParent $(e.currentTarget) - - shouldRender: (e, buttonParentElement) -> - (not buttonParentElement.hasClass(EMPTY_CELL_CLASS) and \ - not buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) and \ - $(COMMENT_BUTTON_CLASS, buttonParentElement).length is 0) - -$.fn.filesCommentButton = -> - return unless this and @parent().data('can-create-note')? - - @each -> - unless $.data this, 'filesCommentButton' - $.data this, 'filesCommentButton', new FilesCommentButton $(this) diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js new file mode 100644 index 00000000000..c8a02d6fa15 --- /dev/null +++ b/app/assets/javascripts/flash.js @@ -0,0 +1,43 @@ +(function() { + this.Flash = (function() { + var hideFlash; + + hideFlash = function() { + return $(this).fadeOut(); + }; + + function Flash(message, type, parent) { + var flash, textDiv; + if (type == null) { + type = 'alert'; + } + if (parent == null) { + parent = null; + } + if (parent) { + this.flashContainer = parent.find('.flash-container'); + } else { + this.flashContainer = $('.flash-container-page'); + } + this.flashContainer.html(''); + flash = $('<div/>', { + "class": "flash-" + type + }); + flash.on('click', hideFlash); + textDiv = $('<div/>', { + "class": 'flash-text', + text: message + }); + textDiv.appendTo(flash); + if (this.flashContainer.parent().hasClass('content-wrapper')) { + textDiv.addClass('container-fluid container-limited'); + } + flash.appendTo(this.flashContainer); + this.flashContainer.show(); + } + + return Flash; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee deleted file mode 100644 index 5a493041538..00000000000 --- a/app/assets/javascripts/flash.js.coffee +++ /dev/null @@ -1,28 +0,0 @@ -class @Flash - hideFlash = -> $(@).fadeOut() - - constructor: (message, type = 'alert', parent = null)-> - if parent - @flashContainer = parent.find('.flash-container') - else - @flashContainer = $('.flash-container-page') - - @flashContainer.html('') - - flash = $('<div/>', - class: "flash-#{type}" - ) - flash.on 'click', hideFlash - - textDiv = $('<div/>', - class: 'flash-text', - text: message - ) - textDiv.appendTo(flash) - - if @flashContainer.parent().hasClass('content-wrapper') - textDiv.addClass('container-fluid container-limited') - - flash.appendTo(@flashContainer) - @flashContainer.show() - diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js new file mode 100644 index 00000000000..41f4c1914f2 --- /dev/null +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -0,0 +1,272 @@ +(function() { + if (window.GitLab == null) { + window.GitLab = {}; + } + + GitLab.GfmAutoComplete = { + dataLoading: false, + dataLoaded: false, + cachedData: {}, + dataSource: '', + Emoji: { + template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' + }, + Members: { + template: '<li>${username} <small>${title}</small></li>' + }, + Labels: { + template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' + }, + Issues: { + template: '<li><small>${id}</small> ${title}</li>' + }, + Milestones: { + template: '<li>${title}</li>' + }, + Loading: { + template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + }, + DefaultOptions: { + sorter: function(query, items, searchKey) { + if ((items[0].name != null) && items[0].name === 'loading') { + return items; + } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); + }, + filter: function(query, data, searchKey) { + if (data[0] === 'loading') { + return data; + } + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); + }, + beforeInsert: function(value) { + if (!GitLab.GfmAutoComplete.dataLoaded) { + return this.at; + } else { + return value; + } + } + }, + setup: function(wrap) { + this.input = $('.js-gfm-input'); + this.destroyAtWho(); + this.setupAtWho(); + if (this.dataSource) { + if (!this.dataLoading && !this.cachedData) { + this.dataLoading = true; + setTimeout((function(_this) { + return function() { + var fetch; + fetch = _this.fetchData(_this.dataSource); + return fetch.done(function(data) { + _this.dataLoading = false; + return _this.loadData(data); + }); + }; + })(this), 1000); + } + if (this.cachedData != null) { + return this.loadData(this.cachedData); + } + } + }, + setupAtWho: function() { + this.input.atwho({ + at: ':', + displayTpl: (function(_this) { + return function(value) { + if (value.path != null) { + return _this.Emoji.template; + } else { + return _this.Loading.template; + } + }; + })(this), + insertTpl: ':${name}:', + data: ['loading'], + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert + } + }); + this.input.atwho({ + at: '@', + displayTpl: (function(_this) { + return function(value) { + if (value.username != null) { + return _this.Members.template; + } else { + return _this.Loading.template; + } + }; + })(this), + insertTpl: '${atwho-at}${username}', + searchKey: 'search', + data: ['loading'], + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(members) { + return $.map(members, function(m) { + var title; + if (m.username == null) { + return m; + } + title = m.name; + if (m.count) { + title += " (" + m.count + ")"; + } + return { + username: m.username, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) + }; + }); + } + } + }); + this.input.atwho({ + at: '#', + alias: 'issues', + searchKey: 'search', + displayTpl: (function(_this) { + return function(value) { + if (value.title != null) { + return _this.Issues.template; + } else { + return _this.Loading.template; + } + }; + })(this), + data: ['loading'], + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(issues) { + return $.map(issues, function(i) { + if (i.title == null) { + return i; + } + return { + id: i.iid, + title: sanitize(i.title), + search: i.iid + " " + i.title + }; + }); + } + } + }); + this.input.atwho({ + at: '%', + alias: 'milestones', + searchKey: 'search', + displayTpl: (function(_this) { + return function(value) { + if (value.title != null) { + return _this.Milestones.template; + } else { + return _this.Loading.template; + } + }; + })(this), + insertTpl: '${atwho-at}"${title}"', + data: ['loading'], + callbacks: { + beforeSave: function(milestones) { + return $.map(milestones, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: "" + m.title + }; + }); + } + } + }); + this.input.atwho({ + at: '!', + alias: 'mergerequests', + searchKey: 'search', + displayTpl: (function(_this) { + return function(value) { + if (value.title != null) { + return _this.Issues.template; + } else { + return _this.Loading.template; + } + }; + })(this), + data: ['loading'], + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(merges) { + return $.map(merges, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: m.iid + " " + m.title + }; + }); + } + } + }); + return this.input.atwho({ + at: '~', + alias: 'labels', + searchKey: 'search', + displayTpl: this.Labels.template, + insertTpl: '${atwho-at}${title}', + callbacks: { + beforeSave: function(merges) { + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; + return $.map(merges, function(m) { + return { + title: sanitizeLabelTitle(m.title), + color: m.color, + search: "" + m.title + }; + }); + } + } + }); + }, + destroyAtWho: function() { + return this.input.atwho('destroy'); + }, + fetchData: function(dataSource) { + return $.getJSON(dataSource); + }, + loadData: function(data) { + this.cachedData = data; + this.dataLoaded = true; + this.input.atwho('load', '@', data.members); + this.input.atwho('load', 'issues', data.issues); + this.input.atwho('load', 'milestones', data.milestones); + this.input.atwho('load', 'mergerequests', data.mergerequests); + this.input.atwho('load', ':', data.emojis); + this.input.atwho('load', '~', data.labels); + return $(':focus').trigger('keyup'); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee deleted file mode 100644 index 4a851d9c9fb..00000000000 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ /dev/null @@ -1,228 +0,0 @@ -# Creates the variables for setting up GFM auto-completion - -window.GitLab ?= {} -GitLab.GfmAutoComplete = - dataLoading: false - 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>' - - Loading: - template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' - - DefaultOptions: - sorter: (query, items, searchKey) -> - return items if items[0].name? and items[0].name is 'loading' - - $.fn.atwho.default.callbacks.sorter(query, items, searchKey) - filter: (query, data, searchKey) -> - return data if data[0] is 'loading' - - $.fn.atwho.default.callbacks.filter(query, data, searchKey) - beforeInsert: (value) -> - if not GitLab.GfmAutoComplete.dataLoaded - @at - else - value - - # Add GFM auto-completion to all input fields, that accept GFM input. - setup: (wrap) -> - @input = $('.js-gfm-input') - - # destroy previous instances - @destroyAtWho() - - # set up instances - @setupAtWho() - - if @dataSource - if not @dataLoading and not @cachedData - @dataLoading = true - - # 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 - setTimeout( => - fetch = @fetchData(@dataSource) - fetch.done (data) => - @dataLoading = false - @loadData(data) - , 1000) - - if @cachedData? - @loadData(@cachedData) - - setupAtWho: -> - # Emoji - @input.atwho - at: ':' - displayTpl: (value) => - if value.path? - @Emoji.template - else - @Loading.template - insertTpl: ':${name}:' - data: ['loading'] - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - - # Team Members - @input.atwho - at: '@' - displayTpl: (value) => - if value.username? - @Members.template - else - @Loading.template - insertTpl: '${atwho-at}${username}' - searchKey: 'search' - data: ['loading'] - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - beforeSave: (members) -> - $.map members, (m) -> - return m if not m.username? - - title = m.name - title += " (#{m.count})" if m.count - - username: m.username - title: sanitize(title) - search: sanitize("#{m.username} #{m.name}") - - @input.atwho - at: '#' - alias: 'issues' - searchKey: 'search' - displayTpl: (value) => - if value.title? - @Issues.template - else - @Loading.template - data: ['loading'] - insertTpl: '${atwho-at}${id}' - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - beforeSave: (issues) -> - $.map issues, (i) -> - return i if not i.title? - - id: i.iid - title: sanitize(i.title) - search: "#{i.iid} #{i.title}" - - @input.atwho - at: '%' - alias: 'milestones' - searchKey: 'search' - displayTpl: (value) => - if value.title? - @Milestones.template - else - @Loading.template - insertTpl: '${atwho-at}"${title}"' - data: ['loading'] - callbacks: - beforeSave: (milestones) -> - $.map milestones, (m) -> - return m if not m.title? - - id: m.iid - title: sanitize(m.title) - search: "#{m.title}" - - @input.atwho - at: '!' - alias: 'mergerequests' - searchKey: 'search' - displayTpl: (value) => - if value.title? - @Issues.template - else - @Loading.template - data: ['loading'] - insertTpl: '${atwho-at}${id}' - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - beforeSave: (merges) -> - $.map merges, (m) -> - return m if not m.title? - - id: m.iid - title: sanitize(m.title) - search: "#{m.iid} #{m.title}" - - @input.atwho - at: '~' - alias: 'labels' - searchKey: 'search' - displayTpl: @Labels.template - insertTpl: '${atwho-at}${title}' - callbacks: - beforeSave: (merges) -> - sanitizeLabelTitle = (title)-> - if /[\w\?&]+\s+[\w\?&]+/g.test(title) - "\"#{sanitize(title)}\"" - else - sanitize(title) - - $.map merges, (m) -> - title: sanitizeLabelTitle(m.title) - color: m.color - search: "#{m.title}" - - destroyAtWho: -> - @input.atwho('destroy') - - fetchData: (dataSource) -> - $.getJSON(dataSource) - - loadData: (data) -> - @cachedData = data - @dataLoaded = true - - # load members - @input.atwho 'load', '@', data.members - # load issues - @input.atwho 'load', 'issues', data.issues - # load milestones - @input.atwho 'load', 'milestones', data.milestones - # load merge requests - @input.atwho 'load', 'mergerequests', data.mergerequests - # load emojis - @input.atwho 'load', ':', data.emojis - # load labels - @input.atwho 'load', '~', data.labels - - # This trigger at.js again - # otherwise we would be stuck with loading until the user types - $(':focus').trigger('keyup') diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js new file mode 100644 index 00000000000..c5d92831fbe --- /dev/null +++ b/app/assets/javascripts/gl_dropdown.js @@ -0,0 +1,705 @@ +(function() { + var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + GitLabDropdownFilter = (function() { + var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; + + BLUR_KEYCODES = [27, 40]; + + ARROW_KEY_CODES = [38, 40]; + + HAS_VALUE_CLASS = "has-value"; + + function GitLabDropdownFilter(input, options) { + var $clearButton, $inputContainer, ref, timeout; + this.input = input; + this.options = options; + this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; + $inputContainer = this.input.parent(); + $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + this.indeterminateIds = []; + $clearButton.on('click', (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.input.val('').trigger('keyup').focus(); + }; + })(this)); + timeout = ""; + this.input.on("keyup", (function(_this) { + return function(e) { + var keyCode; + keyCode = e.which; + if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) { + return; + } + if (_this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.addClass(HAS_VALUE_CLASS); + } else if (_this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.removeClass(HAS_VALUE_CLASS); + } + if (keyCode === 13) { + return false; + } + if (_this.options.remote) { + clearTimeout(timeout); + return timeout = setTimeout(function() { + var blur_field; + blur_field = _this.shouldBlur(keyCode); + if (blur_field && _this.filterInputBlur) { + _this.input.blur(); + } + return _this.options.query(_this.input.val(), function(data) { + return _this.options.callback(data); + }); + }, 250); + } else { + return _this.filter(_this.input.val()); + } + }; + })(this)); + } + + GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { + return BLUR_KEYCODES.indexOf(keyCode) >= 0; + }; + + GitLabDropdownFilter.prototype.filter = function(search_text) { + var data, elements, group, key, results, tmp; + if (this.options.onFilter) { + this.options.onFilter(search_text); + } + data = this.options.data(); + if ((data != null) && !this.options.filterByText) { + results = data; + if (search_text !== '') { + if (_.isArray(data)) { + results = fuzzaldrinPlus.filter(data, search_text, { + key: this.options.keys + }); + } else { + if (gl.utils.isObject(data)) { + results = {}; + for (key in data) { + group = data[key]; + tmp = fuzzaldrinPlus.filter(group, search_text, { + key: this.options.keys + }); + if (tmp.length) { + results[key] = tmp.map(function(item) { + return item; + }); + } + } + } + } + } + return this.options.callback(results); + } else { + elements = this.options.elements(); + if (search_text) { + return elements.each(function() { + var $el, matches; + $el = $(this); + matches = fuzzaldrinPlus.match($el.text().trim(), search_text); + if (!$el.is('.dropdown-header')) { + if (matches.length) { + return $el.show(); + } else { + return $el.hide(); + } + } + }); + } else { + return elements.show(); + } + } + }; + + return GitLabDropdownFilter; + + })(); + + GitLabDropdownRemote = (function() { + function GitLabDropdownRemote(dataEndpoint, options) { + this.dataEndpoint = dataEndpoint; + this.options = options; + } + + GitLabDropdownRemote.prototype.execute = function() { + if (typeof this.dataEndpoint === "string") { + return this.fetchData(); + } else if (typeof this.dataEndpoint === "function") { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + return this.dataEndpoint("", (function(_this) { + return function(data) { + if (_this.options.success) { + _this.options.success(data); + } + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this)); + } + }; + + GitLabDropdownRemote.prototype.fetchData = function() { + return $.ajax({ + url: this.dataEndpoint, + dataType: this.options.dataType, + beforeSend: (function(_this) { + return function() { + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this), + success: (function(_this) { + return function(data) { + if (_this.options.success) { + return _this.options.success(data); + } + }; + })(this) + }); + }; + + return GitLabDropdownRemote; + + })(); + + GitLabDropdown = (function() { + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex; + + LOADING_CLASS = "is-loading"; + + PAGE_TWO_CLASS = "is-page-two"; + + ACTIVE_CLASS = "is-active"; + + INDETERMINATE_CLASS = "is-indeterminate"; + + currentIndex = -1; + + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; + + function GitLabDropdown(el1, options) { + var ref, ref1, ref2, ref3, searchFields, selector, self; + this.el = el1; + this.options = options; + this.updateLabel = bind(this.updateLabel, this); + this.hidden = bind(this.hidden, this); + this.opened = bind(this.opened, this); + this.shouldPropagate = bind(this.shouldPropagate, this); + self = this; + selector = $(this.el).data("target"); + this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + 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; + self = this; + if (_.isString(this.filterInput)) { + this.filterInput = this.getElement(this.filterInput); + } + searchFields = this.options.search ? this.options.search.fields : []; + if (this.options.data) { + if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { + this.fullData = this.options.data; + this.parseData(this.options.data); + } else { + this.remote = new GitLabDropdownRemote(this.options.data, { + dataType: this.options.dataType, + beforeSend: this.toggleLoading.bind(this), + success: (function(_this) { + return function(data) { + _this.fullData = data; + _this.parseData(_this.fullData); + if (_this.options.filterable && _this.filter && _this.filter.input) { + return _this.filter.input.trigger('keyup'); + } + }; + })(this) + }); + } + } + if (this.options.filterable) { + this.filter = new GitLabDropdownFilter(this.filterInput, { + filterInputBlur: this.filterInputBlur, + filterByText: this.options.filterByText, + onFilter: this.options.onFilter, + remote: this.options.filterRemote, + query: this.options.data, + keys: searchFields, + elements: (function(_this) { + return function() { + selector = '.dropdown-content li:not(.divider)'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + return $(selector); + }; + })(this), + data: (function(_this) { + return function() { + return _this.fullData; + }; + })(this), + callback: (function(_this) { + return function(data) { + _this.parseData(data); + if (_this.filterInput.val() !== '') { + selector = '.dropdown-content li:not(.divider):visible'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + $(selector, _this.dropdown).first().find('a').addClass('is-focused'); + return currentIndex = 0; + } + }; + })(this) + }); + } + 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) { + if (e.which === 27) { + return $('.dropdown-menu-close', _this.dropdown).trigger('click'); + } + }; + })(this)); + this.dropdown.on('blur', 'a', (function(_this) { + return function(e) { + var $dropdownMenu, $relatedTarget; + if (e.relatedTarget != null) { + $relatedTarget = $(e.relatedTarget); + $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); + if ($dropdownMenu.length === 0) { + return _this.dropdown.removeClass('open'); + } + } + }; + })(this)); + if (this.dropdown.find(".dropdown-toggle-page").length) { + this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.togglePage(); + }; + })(this)); + } + if (this.options.selectable) { + selector = ".dropdown-content a"; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content a"; + } + this.dropdown.on("click", selector, function(e) { + var $el, selected; + $el = $(this); + selected = self.rowClicked($el); + if (self.options.clicked) { + self.options.clicked(selected, $el, e); + } + return $el.trigger('blur'); + }); + } + } + + GitLabDropdown.prototype.getElement = function(selector) { + return this.dropdown.find(selector); + }; + + GitLabDropdown.prototype.toggleLoading = function() { + return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); + }; + + GitLabDropdown.prototype.togglePage = function() { + var menu; + menu = $('.dropdown-menu', this.dropdown); + if (menu.hasClass(PAGE_TWO_CLASS)) { + if (this.remote) { + this.remote.execute(); + } + } + menu.toggleClass(PAGE_TWO_CLASS); + return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); + }; + + GitLabDropdown.prototype.parseData = function(data) { + var full_html, groupData, html, name; + this.renderedData = data; + if (this.options.filterable && data.length === 0) { + html = [this.noResults()]; + } else { + if (gl.utils.isObject(data)) { + html = []; + for (name in data) { + groupData = data[name]; + html.push(this.renderItem({ + header: name + }, name)); + this.renderData(groupData, name).map(function(item) { + return html.push(item); + }); + } + } else { + html = this.renderData(data); + } + } + full_html = this.renderMenu(html); + return this.appendMenu(full_html); + }; + + GitLabDropdown.prototype.renderData = function(data, group) { + if (group == null) { + group = false; + } + return data.map((function(_this) { + return function(obj, index) { + return _this.renderItem(obj, group, index); + }; + })(this)); + }; + + GitLabDropdown.prototype.shouldPropagate = function(e) { + var $target; + if (this.options.multiSelect) { + $target = $(e.target); + if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) { + e.stopPropagation(); + return false; + } else { + return true; + } + } + }; + + GitLabDropdown.prototype.opened = function() { + var contentHtml; + this.addArrowKeyEvent(); + if (this.options.setIndeterminateIds) { + this.options.setIndeterminateIds.call(this); + } + if (this.options.setActiveIds) { + this.options.setActiveIds.call(this); + } + if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + this.parseData(this.fullData); + } + contentHtml = $('.dropdown-content', this.dropdown).html(); + if (this.remote && contentHtml === "") { + this.remote.execute(); + } + if (this.options.filterable) { + this.filterInput.focus(); + } + return this.dropdown.trigger('shown.gl.dropdown'); + }; + + GitLabDropdown.prototype.hidden = function(e) { + var $input; + this.removeArrayKeyEvent(); + $input = this.dropdown.find(".dropdown-input-field"); + if (this.options.filterable) { + $input.blur().val(""); + } + if (!this.options.persistWhenHide) { + $input.trigger("keyup"); + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); + } + if (this.options.hidden) { + this.options.hidden.call(this, e); + } + return this.dropdown.trigger('hidden.gl.dropdown'); + }; + + GitLabDropdown.prototype.renderMenu = function(html) { + var menu_html; + menu_html = ""; + if (this.options.renderMenu) { + menu_html = this.options.renderMenu(html); + } else { + menu_html = $('<ul />').append(html); + } + return menu_html; + }; + + GitLabDropdown.prototype.appendMenu = function(html) { + var selector; + selector = '.dropdown-content'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content"; + } + return $(selector, this.dropdown).empty().append(html); + }; + + GitLabDropdown.prototype.renderItem = function(data, group, index) { + var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value; + if (group == null) { + group = false; + } + if (index == null) { + index = false; + } + html = ""; + if (data === "divider") { + return "<li class='divider'></li>"; + } + if (data === "separator") { + return "<li class='separator'></li>"; + } + if (data.header != null) { + return "<li class='dropdown-header'>" + data.header + "</li>"; + } + if (this.options.renderRow) { + html = this.options.renderRow.call(this.options, data, this); + } else { + if (!selected) { + value = this.options.id ? this.options.id(data) : data.id; + fieldName = this.options.fieldName; + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + if (field.length) { + selected = true; + } + } + if (this.options.url != null) { + url = this.options.url(data); + } else { + url = data.url != null ? data.url : '#'; + } + if (this.options.text != null) { + text = this.options.text(data); + } else { + text = data.text != null ? data.text : ''; + } + cssClass = ""; + if (selected) { + cssClass = "is-active"; + } + if (this.highlight) { + text = this.highlightTextMatches(text, this.filterInput.val()); + } + if (group) { + groupAttrs = "data-group='" + group + "' data-index='" + index + "'"; + } else { + groupAttrs = ''; + } + html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>"; + } + return html; + }; + + GitLabDropdown.prototype.highlightTextMatches = function(text, term) { + var occurrences; + occurrences = fuzzaldrinPlus.match(text, term); + return text.split('').map(function(character, i) { + if (indexOf.call(occurrences, i) >= 0) { + return "<b>" + character + "</b>"; + } else { + return character; + } + }).join(''); + }; + + GitLabDropdown.prototype.noResults = function() { + var html; + return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; + }; + + GitLabDropdown.prototype.highlightRow = function(index) { + var selector; + if (this.filterInput.val() !== "") { + selector = '.dropdown-content li:first-child a'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content li:first-child a"; + } + return this.getElement(selector).addClass('is-focused'); + } + }; + + GitLabDropdown.prototype.rowClicked = function(el) { + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + fieldName = this.options.fieldName; + isInput = $(this.el).is('input'); + if (this.renderedData) { + groupName = el.data('group'); + if (groupName) { + selectedIndex = el.data('index'); + selectedObject = this.renderedData[groupName][selectedIndex]; + } else { + selectedIndex = el.closest('li').index(); + selectedObject = this.renderedData[selectedIndex]; + } + } + value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; + if (isInput) { + field = $(this.el); + } else { + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + } + if (el.hasClass(ACTIVE_CLASS)) { + el.removeClass(ACTIVE_CLASS); + if (isInput) { + field.val(''); + } else { + field.remove(); + } + if (this.options.toggleLabel) { + return this.updateLabel(selectedObject, el, this); + } else { + return selectedObject; + } + } else if (el.hasClass(INDETERMINATE_CLASS)) { + el.addClass(ACTIVE_CLASS); + el.removeClass(INDETERMINATE_CLASS); + if (value == null) { + field.remove(); + } + if (!field.length && fieldName) { + this.addInput(fieldName, value); + } + return selectedObject; + } else { + if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { + this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); + if (!isInput) { + this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); + } + } + if (value == null) { + field.remove(); + } + el.addClass(ACTIVE_CLASS); + if (this.options.toggleLabel) { + this.updateLabel(selectedObject, el, this); + } + if (value != null) { + if (!field.length && fieldName) { + this.addInput(fieldName, value); + } else { + field.val(value).trigger('change'); + } + } + return selectedObject; + } + }; + + GitLabDropdown.prototype.addInput = function(fieldName, value) { + var $input; + $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); + if (this.options.inputId != null) { + $input.attr('id', this.options.inputId); + } + return this.dropdown.before($input); + }; + + GitLabDropdown.prototype.selectRowAtIndex = function(e, index) { + var $el, selector; + selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a"; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + $el = $(selector, this.dropdown); + if ($el.length) { + e.preventDefault(); + e.stopImmediatePropagation(); + return $el.first().trigger('click'); + } + }; + + GitLabDropdown.prototype.addArrowKeyEvent = function() { + var $input, ARROW_KEY_CODES, selector; + ARROW_KEY_CODES = [38, 40]; + $input = this.dropdown.find(".dropdown-input-field"); + selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + return $('body').on('keydown', (function(_this) { + return function(e) { + var $listItems, PREV_INDEX, currentKeyCode; + currentKeyCode = e.which; + if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { + e.preventDefault(); + e.stopImmediatePropagation(); + PREV_INDEX = currentIndex; + $listItems = $(selector, _this.dropdown); + if (currentKeyCode === 40) { + if (currentIndex < ($listItems.length - 1)) { + currentIndex += 1; + } + } else if (currentKeyCode === 38) { + if (currentIndex > 0) { + currentIndex -= 1; + } + } + if (currentIndex !== PREV_INDEX) { + _this.highlightRowAtIndex($listItems, currentIndex); + } + return false; + } + if (currentKeyCode === 13 && currentIndex !== -1) { + return _this.selectRowAtIndex(e, currentIndex); + } + }; + })(this)); + }; + + GitLabDropdown.prototype.removeArrayKeyEvent = function() { + return $('body').off('keydown'); + }; + + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { + var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + $('.is-focused', this.dropdown).removeClass('is-focused'); + $listItem = $listItems.eq(index); + $listItem.find('a:first-child').addClass("is-focused"); + $dropdownContent = $listItem.closest('.dropdown-content'); + dropdownScrollTop = $dropdownContent.scrollTop(); + dropdownContentHeight = $dropdownContent.outerHeight(); + dropdownContentTop = $dropdownContent.prop('offsetTop'); + dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + listItemHeight = $listItem.outerHeight(); + listItemTop = $listItem.prop('offsetTop'); + listItemBottom = listItemTop + listItemHeight; + if (listItemBottom > dropdownContentBottom + dropdownScrollTop) { + return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom); + } else if (listItemTop < dropdownContentTop + dropdownScrollTop) { + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop); + } + }; + + GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { + if (selected == null) { + selected = null; + } + if (el == null) { + el = null; + } + if (instance == null) { + instance = null; + } + return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); + }; + + return GitLabDropdown; + + })(); + + $.fn.glDropdown = function(opts) { + return this.each(function() { + if (!$.data(this, 'glDropdown')) { + return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); + } + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee deleted file mode 100644 index 951530e03a5..00000000000 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ /dev/null @@ -1,625 +0,0 @@ -class GitLabDropdownFilter - BLUR_KEYCODES = [27, 40] - ARROW_KEY_CODES = [38, 40] - HAS_VALUE_CLASS = "has-value" - - constructor: (@input, @options) -> - { - @filterInputBlur = true - } = @options - - $inputContainer = @input.parent() - $clearButton = $inputContainer.find('.js-dropdown-input-clear') - - @indeterminateIds = [] - - # Clear click - $clearButton.on 'click', (e) => - e.preventDefault() - e.stopPropagation() - @input - .val('') - .trigger('keyup') - .focus() - - # Key events - timeout = "" - @input.on "keyup", (e) => - keyCode = e.which - - return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 - - if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS - $inputContainer.addClass HAS_VALUE_CLASS - else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS - $inputContainer.removeClass HAS_VALUE_CLASS - - if keyCode is 13 - return false - - # Only filter asynchronously only if option remote is set - if @options.remote - clearTimeout timeout - timeout = setTimeout => - blur_field = @shouldBlur keyCode - - if blur_field and @filterInputBlur - @input.blur() - - @options.query @input.val(), (data) => - @options.callback(data) - , 250 - else - @filter @input.val() - - shouldBlur: (keyCode) -> - return BLUR_KEYCODES.indexOf(keyCode) >= 0 - - filter: (search_text) -> - @options.onFilter(search_text) if @options.onFilter - data = @options.data() - - if data? and not @options.filterByText - results = data - - if search_text isnt '' - # 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: @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, group of data - tmp = fuzzaldrinPlus.filter(group, search_text, - key: @options.keys - ) - - if tmp.length - results[key] = tmp.map (item) -> item - - @options.callback results - else - elements = @options.elements() - - if search_text - elements.each -> - $el = $(@) - matches = fuzzaldrinPlus.match($el.text().trim(), search_text) - - unless $el.is('.dropdown-header') - if matches.length - $el.show() - else - $el.hide() - else - elements.show() - -class GitLabDropdownRemote - constructor: (@dataEndpoint, @options) -> - - execute: -> - if typeof @dataEndpoint is "string" - @fetchData() - else if typeof @dataEndpoint is "function" - if @options.beforeSend - @options.beforeSend() - - # Fetch the data by calling the data funcfion - @dataEndpoint "", (data) => - if @options.success - @options.success(data) - - if @options.beforeSend - @options.beforeSend() - - # Fetch the data through ajax if the data is a string - fetchData: -> - $.ajax( - url: @dataEndpoint, - dataType: @options.dataType, - beforeSend: => - if @options.beforeSend - @options.beforeSend() - success: (data) => - if @options.success - @options.success(data) - ) - -class GitLabDropdown - LOADING_CLASS = "is-loading" - PAGE_TWO_CLASS = "is-page-two" - ACTIVE_CLASS = "is-active" - INDETERMINATE_CLASS = "is-indeterminate" - currentIndex = -1 - - FILTER_INPUT = '.dropdown-input .dropdown-input-field' - - constructor: (@el, @options) -> - self = @ - selector = $(@el).data "target" - @dropdown = if selector? then $(selector) else $(@el).parent() - - # Set Defaults - { - # If no input is passed create a default one - @filterInput = @getElement(FILTER_INPUT) - @highlight = false - @filterInputBlur = true - } = @options - - self = @ - - # If selector was passed - if _.isString(@filterInput) - @filterInput = @getElement(@filterInput) - - searchFields = if @options.search then @options.search.fields else []; - - if @options.data - # If we provided data - # data could be an array of objects or a group of arrays - if _.isObject(@options.data) and not _.isFunction(@options.data) - @fullData = @options.data - @parseData @options.data - else - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data - - @parseData @fullData - - @filter.input.trigger('keyup') if @options.filterable and @filter and @filter.input - } - - # Init filterable - if @options.filterable - @filter = new GitLabDropdownFilter @filterInput, - filterInputBlur: @filterInputBlur - filterByText: @options.filterByText - onFilter: @options.onFilter - remote: @options.filterRemote - query: @options.data - keys: searchFields - elements: => - selector = '.dropdown-content li:not(.divider)' - - if @dropdown.find('.dropdown-toggle-page').length - selector = ".dropdown-page-one #{selector}" - - return $(selector) - data: => - return @fullData - callback: (data) => - currentIndex = -1 - @parseData data - - # Event listeners - - @dropdown.on "shown.bs.dropdown", @opened - @dropdown.on "hidden.bs.dropdown", @hidden - $(@el).on "update.label", @updateLabel - @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate - @dropdown.on 'keyup', (e) => - if e.which is 27 # Escape key - $('.dropdown-menu-close', @dropdown).trigger 'click' - @dropdown.on 'blur', 'a', (e) => - if e.relatedTarget? - $relatedTarget = $(e.relatedTarget) - $dropdownMenu = $relatedTarget.closest('.dropdown-menu') - - if $dropdownMenu.length is 0 - @dropdown.removeClass('open') - - if @dropdown.find(".dropdown-toggle-page").length - @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => - e.preventDefault() - e.stopPropagation() - - @togglePage() - - if @options.selectable - selector = ".dropdown-content a" - - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content a" - - @dropdown.on "click", selector, (e) -> - $el = $(@) - selected = self.rowClicked $el - - if self.options.clicked - self.options.clicked(selected, $el, e) - - $el.trigger('blur') - - # Finds an element inside wrapper element - getElement: (selector) -> - @dropdown.find selector - - toggleLoading: -> - $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS - - togglePage: -> - menu = $('.dropdown-menu', @dropdown) - - if menu.hasClass(PAGE_TWO_CLASS) - if @remote - @remote.execute() - - menu.toggleClass PAGE_TWO_CLASS - - # Focus first visible input on active page - @dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus() - - parseData: (data) -> - @renderedData = data - - if @options.filterable and data.length is 0 - # render no matching results - html = [@noResults()] - else - # Handle array groups - if gl.utils.isObject data - html = [] - for name, groupData of data - # Add header for each group - html.push(@renderItem(header: name, name)) - - @renderData(groupData, name) - .map (item) -> - html.push item - else - # Render each row - html = @renderData(data) - - # Render the full menu - full_html = @renderMenu(html) - - @appendMenu(full_html) - - renderData: (data, group = false) -> - data.map (obj, index) => - return @renderItem(obj, group, index) - - shouldPropagate: (e) => - if @options.multiSelect - $target = $(e.target) - - if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link') - e.stopPropagation() - return false - else - return true - - opened: => - @addArrowKeyEvent() - - if @options.setIndeterminateIds - @options.setIndeterminateIds.call(@) - - if @options.setActiveIds - @options.setActiveIds.call(@) - - # Makes indeterminate items effective - if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - @parseData @fullData - - contentHtml = $('.dropdown-content', @dropdown).html() - if @remote && contentHtml is "" - @remote.execute() - - if @options.filterable - @filterInput.focus() - - @dropdown.trigger('shown.gl.dropdown') - - hidden: (e) => - @removeArrayKeyEvent() - - $input = @dropdown.find(".dropdown-input-field") - - if @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 not @options.persistWhenHide - $input.trigger("keyup") - - if @dropdown.find(".dropdown-toggle-page").length - $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS - - if @options.hidden - @options.hidden.call(@,e) - - @dropdown.trigger('hidden.gl.dropdown') - - - # Render the full menu - renderMenu: (html) -> - menu_html = "" - - if @options.renderMenu - menu_html = @options.renderMenu(html) - else - menu_html = $('<ul />') - .append(html) - - return menu_html - - # Append the menu into the dropdown - appendMenu: (html) -> - selector = '.dropdown-content' - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content" - $(selector, @dropdown) - .empty() - .append(html) - - # Render the row - renderItem: (data, group = false, index = false) -> - html = "" - - # Divider - return "<li class='divider'></li>" if data is "divider" - - # Separator is a full-width divider - return "<li class='separator'></li>" if data is "separator" - - # Header - return "<li class='dropdown-header'>#{data.header}</li>" if data.header? - - if @options.renderRow - # Call the render function - html = @options.renderRow.call(@options, data, @) - else - if not selected - value = if @options.id then @options.id(data) else data.id - fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - if field.length - selected = true - - # Set URL - if @options.url? - url = @options.url(data) - else - url = if data.url? then data.url else '#' - - # Set Text - if @options.text? - text = @options.text(data) - else - text = if data.text? then data.text else '' - - cssClass = ""; - - if selected - cssClass = "is-active" - - if @highlight - text = @highlightTextMatches(text, @filterInput.val()) - - if group - groupAttrs = "data-group='#{group}' data-index='#{index}'" - else - groupAttrs = '' - - html = "<li> - <a href='#{url}' #{groupAttrs} class='#{cssClass}'> - #{text} - </a> - </li>" - - return html - - highlightTextMatches: (text, term) -> - occurrences = fuzzaldrinPlus.match(text, term) - text.split('').map((character, i) -> - if i in occurrences then "<b>#{character}</b>" else character - ).join('') - - noResults: -> - html = "<li class='dropdown-menu-empty-link'> - <a href='#' class='is-focused'> - No matching results. - </a> - </li>" - - highlightRow: (index) -> - if @filterInput.val() isnt "" - selector = '.dropdown-content li:first-child a' - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content li:first-child a" - - @getElement(selector).addClass 'is-focused' - - rowClicked: (el) -> - fieldName = @options.fieldName - isInput = $(@el).is('input') - - if @renderedData - groupName = el.data('group') - if groupName - selectedIndex = el.data('index') - selectedObject = @renderedData[groupName][selectedIndex] - else - selectedIndex = el.closest('li').index() - selectedObject = @renderedData[selectedIndex] - - value = if @options.id then @options.id(selectedObject, el) else selectedObject.id - - if isInput - field = $(@el) - else - field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - - if el.hasClass(ACTIVE_CLASS) - el.removeClass(ACTIVE_CLASS) - - if isInput - field.val('') - else - field.remove() - - # Toggle the dropdown label - if @options.toggleLabel - @updateLabel(selectedObject, el, @) - else - selectedObject - else if el.hasClass(INDETERMINATE_CLASS) - el.addClass ACTIVE_CLASS - el.removeClass INDETERMINATE_CLASS - - if not value? - field.remove() - - if not field.length and fieldName - @addInput(fieldName, value) - - return selectedObject - else - if not @options.multiSelect or el.hasClass('dropdown-clear-active') - @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS - - unless isInput - @dropdown.parent().find("input[name='#{fieldName}']").remove() - - if !value? - field.remove() - - # Toggle active class for the tick mark - el.addClass ACTIVE_CLASS - - # Toggle the dropdown label - if @options.toggleLabel - @updateLabel(selectedObject, el, @) - if value? - if !field.length and fieldName - @addInput(fieldName, value) - else - field - .val value - .trigger 'change' - - return selectedObject - - addInput: (fieldName, value)-> - # Create hidden input for form - $input = $('<input>').attr('type', 'hidden') - .attr('name', fieldName) - .val(value) - - if @options.inputId? - $input.attr('id', @options.inputId) - - @dropdown.before $input - - selectRowAtIndex: (e, index) -> - selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" - - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one #{selector}" - - # simulate a click on the first link - $el = $(selector, @dropdown) - - if $el.length - e.preventDefault() - e.stopImmediatePropagation() - $el.first().trigger('click') - - addArrowKeyEvent: -> - ARROW_KEY_CODES = [38, 40] - $input = @dropdown.find(".dropdown-input-field") - - selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)' - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one #{selector}" - - $('body').on 'keydown', (e) => - currentKeyCode = e.which - - if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0 - e.preventDefault() - e.stopImmediatePropagation() - - PREV_INDEX = currentIndex - $listItems = $(selector, @dropdown) - - # if @options.filterable - # $input.blur() - - if currentKeyCode is 40 - # Move down - currentIndex += 1 if currentIndex < ($listItems.length - 1) - else if currentKeyCode is 38 - # Move up - currentIndex -= 1 if currentIndex > 0 - - @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX - - return false - - if currentKeyCode is 13 and currentIndex isnt -1 - @selectRowAtIndex e, currentIndex - - removeArrayKeyEvent: -> - $('body').off 'keydown' - - highlightRowAtIndex: ($listItems, index) -> - # Remove the class for the previously focused row - $('.is-focused', @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 listItemBottom > dropdownContentBottom + dropdownScrollTop - # Scroll the dropdown content down - $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom) - else if listItemTop < dropdownContentTop + dropdownScrollTop - # Scroll the dropdown content up - $dropdownContent.scrollTop(listItemTop - dropdownContentTop) - - updateLabel: (selected = null, el = null, instance = null) => - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el, instance) - -$.fn.glDropdown = (opts) -> - return @.each -> - if (!$.data @, 'glDropdown') - $.data(@, 'glDropdown', new GitLabDropdown @, opts) diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js new file mode 100644 index 00000000000..6ac7564a848 --- /dev/null +++ b/app/assets/javascripts/gl_form.js @@ -0,0 +1,53 @@ +(function() { + this.GLForm = (function() { + function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + this.destroy(); + this.setupForm(); + this.form.data('gl-form', this); + } + + GLForm.prototype.destroy = function() { + this.clearEventListeners(); + return this.form.data('gl-form', null); + }; + + GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + GitLab.GfmAutoComplete.setup(); + new DropzoneInput(this.form); + autosize(this.textarea); + this.addEventListeners(); + gl.text.init(this.form); + } + this.form.find('.js-note-discard').hide(); + return this.form.show(); + }; + + GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); + }; + + GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); + }; + + return GLForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee deleted file mode 100644 index 77512d187c9..00000000000 --- a/app/assets/javascripts/gl_form.js.coffee +++ /dev/null @@ -1,54 +0,0 @@ -class @GLForm - constructor: (@form) -> - @textarea = @form.find('textarea.js-gfm-input') - - # Before we start, we should clean up any previous data for this form - @destroy() - - # Setup the form - @setupForm() - - @form.data 'gl-form', @ - - destroy: -> - # Clean form listeners - @clearEventListeners() - @form.data 'gl-form', null - - setupForm: -> - isNewForm = @form.is(':not(.gfm-form)') - - @form.removeClass 'js-new-note-form' - - if isNewForm - @form.find('.div-dropzone').remove() - @form.addClass('gfm-form') - disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button') - - # remove notify commit author checkbox for non-commit notes - GitLab.GfmAutoComplete.setup() - new DropzoneInput(@form) - - autosize(@textarea) - - # form and textarea event listeners - @addEventListeners() - - gl.text.init(@form) - - # hide discard button - @form.find('.js-note-discard').hide() - - @form.show() - - clearEventListeners: -> - @textarea.off 'focus' - @textarea.off 'blur' - gl.text.removeListeners(@form) - - addEventListeners: -> - @textarea.on 'focus', -> - $(@).closest('.md-area').addClass 'is-focused' - - @textarea.on 'blur', -> - $(@).closest('.md-area').removeClass 'is-focused' diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js new file mode 100644 index 00000000000..b95faadc8e7 --- /dev/null +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -0,0 +1,7 @@ + +/*= require_tree . */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/graphs/graphs_bundle.js.coffee b/app/assets/javascripts/graphs/graphs_bundle.js.coffee deleted file mode 100644 index e0f681acf0b..00000000000 --- a/app/assets/javascripts/graphs/graphs_bundle.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -# 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 . diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js new file mode 100644 index 00000000000..f041980bc19 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -0,0 +1,19 @@ +(function() { + this.StatGraph = (function() { + function StatGraph() {} + + StatGraph.log = {}; + + StatGraph.get_log = function() { + return this.log; + }; + + StatGraph.set_log = function(data) { + return this.log = data; + }; + + return StatGraph; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee deleted file mode 100644 index f36c71fd25e..00000000000 --- a/app/assets/javascripts/graphs/stat_graph.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -class @StatGraph - @log: {} - @get_log: -> - @log - @set_log: (data) -> - @log = data diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js new file mode 100644 index 00000000000..927d241b357 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -0,0 +1,112 @@ + +/*= require d3 */ + +(function() { + this.ContributorsStatGraph = (function() { + function ContributorsStatGraph() {} + + ContributorsStatGraph.prototype.init = function(log) { + var author_commits, total_commits; + this.parsed_log = ContributorsStatGraphUtil.parse_log(log); + this.set_current_field("commits"); + total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); + this.add_master_graph(total_commits); + this.add_authors_graph(author_commits); + return this.change_date_header(); + }; + + ContributorsStatGraph.prototype.add_master_graph = function(total_data) { + this.master_graph = new ContributorsMasterGraph(total_data); + return this.master_graph.draw(); + }; + + ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { + var limited_author_data; + this.authors = []; + limited_author_data = author_data.slice(0, 100); + return _.each(limited_author_data, (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $(".contributors-list").append(author_header); + _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + return author_graph.draw(); + }; + })(this)); + }; + + ContributorsStatGraph.prototype.format_author_commit_info = function(author) { + var commits; + commits = $('<span/>', { + "class": 'graph-author-commits-count' + }); + commits.text(author.commits + " commits"); + return $('<span/>').append(commits); + }; + + ContributorsStatGraph.prototype.create_author_header = function(author) { + var author_commit_info, author_commit_info_span, author_email, author_name, list_item; + list_item = $('<li/>', { + "class": 'person', + style: 'display: block;' + }); + author_name = $('<h4>' + author.author_name + '</h4>'); + author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); + author_commit_info_span = $('<span/>', { + "class": 'commits' + }); + author_commit_info = this.format_author_commit_info(author); + author_commit_info_span.html(author_commit_info); + list_item.append(author_name); + list_item.append(author_email); + list_item.append(author_commit_info_span); + return list_item; + }; + + ContributorsStatGraph.prototype.redraw_master = function() { + var total_data; + total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + this.master_graph.set_data(total_data); + return this.master_graph.redraw(); + }; + + ContributorsStatGraph.prototype.redraw_authors = function() { + var author_commits, x_domain; + $("ol").html(""); + x_domain = ContributorsGraph.prototype.x_domain; + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + $(_this.authors[d.author_name].list_item).appendTo("ol"); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + }; + })(this)); + }; + + ContributorsStatGraph.prototype.set_current_field = function(field) { + return this.field = field; + }; + + ContributorsStatGraph.prototype.change_date_header = function() { + var print, print_date_format, x_domain; + x_domain = ContributorsGraph.prototype.x_domain; + print_date_format = d3.time.format("%B %e %Y"); + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); + return $("#date_header").text(print); + }; + + ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { + var author_commit_info, author_list_item; + author_list_item = $(this.authors[author.author_name].list_item); + author_commit_info = this.format_author_commit_info(author); + return author_list_item.find("span").html(author_commit_info); + }; + + return ContributorsStatGraph; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee deleted file mode 100644 index 1d9fae7cf79..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee +++ /dev/null @@ -1,71 +0,0 @@ -#= require d3 - -class @ContributorsStatGraph - init: (log) -> - @parsed_log = ContributorsStatGraphUtil.parse_log(log) - @set_current_field("commits") - total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) - author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field) - @add_master_graph(total_commits) - @add_authors_graph(author_commits) - @change_date_header() - add_master_graph: (total_data) -> - @master_graph = new ContributorsMasterGraph(total_data) - @master_graph.draw() - add_authors_graph: (author_data) -> - @authors = [] - limited_author_data = author_data.slice(0, 100) - _.each(limited_author_data, (d) => - author_header = @create_author_header(d) - $(".contributors-list").append(author_header) - @authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates) - author_graph.draw() - ) - format_author_commit_info: (author) -> - commits = $('<span/>', { - class: 'graph-author-commits-count' - }) - commits.text(author.commits + " commits") - $('<span/>').append(commits) - - create_author_header: (author) -> - list_item = $('<li/>', { - class: 'person' - style: 'display: block;' - }) - author_name = $('<h4>' + author.author_name + '</h4>') - author_email = $('<p class="graph-author-email">' + author.author_email + '</p>') - author_commit_info_span = $('<span/>', { - class: 'commits' - }) - author_commit_info = @format_author_commit_info(author) - author_commit_info_span.html(author_commit_info) - list_item.append(author_name) - list_item.append(author_email) - list_item.append(author_commit_info_span) - list_item - redraw_master: -> - total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) - @master_graph.set_data(total_data) - @master_graph.redraw() - redraw_authors: -> - $("ol").html("") - x_domain = ContributorsGraph.prototype.x_domain - author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain) - _.each(author_commits, (d) => - @redraw_author_commit_info(d) - $(@authors[d.author_name].list_item).appendTo("ol") - @authors[d.author_name].set_data(d.dates) - @authors[d.author_name].redraw() - ) - set_current_field: (field) -> - @field = field - change_date_header: -> - x_domain = ContributorsGraph.prototype.x_domain - print_date_format = d3.time.format("%B %e %Y") - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]) - $("#date_header").text(print) - redraw_author_commit_info: (author) -> - author_list_item = $(@authors[author.author_name].list_item) - author_commit_info = @format_author_commit_info(author) - author_list_item.find("span").html(author_commit_info) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js new file mode 100644 index 00000000000..a646ca1d84f --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -0,0 +1,279 @@ + +/*= require d3 */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ContributorsGraph = (function() { + function ContributorsGraph() {} + + ContributorsGraph.prototype.MARGIN = { + top: 20, + right: 20, + bottom: 30, + left: 50 + }; + + ContributorsGraph.prototype.x_domain = null; + + ContributorsGraph.prototype.y_domain = null; + + ContributorsGraph.prototype.dates = []; + + ContributorsGraph.set_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = data; + }; + + ContributorsGraph.set_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + var ref, ref1; + return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + }) + ]; + }; + + ContributorsGraph.init_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return d.date; + }); + }; + + ContributorsGraph.init_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + var ref, ref1; + return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + }) + ]; + }; + + ContributorsGraph.init_domain = function(data) { + ContributorsGraph.init_x_domain(data); + return ContributorsGraph.init_y_domain(data); + }; + + ContributorsGraph.set_dates = function(data) { + return ContributorsGraph.prototype.dates = data; + }; + + ContributorsGraph.prototype.set_x_domain = function() { + return this.x.domain(this.x_domain); + }; + + ContributorsGraph.prototype.set_y_domain = function() { + return this.y.domain(this.y_domain); + }; + + ContributorsGraph.prototype.set_domain = function() { + this.set_x_domain(); + return this.set_y_domain(); + }; + + ContributorsGraph.prototype.create_scale = function(width, height) { + this.x = d3.time.scale().range([0, width]).clamp(true); + return this.y = d3.scale.linear().range([height, 0]).nice(); + }; + + ContributorsGraph.prototype.draw_x_axis = function() { + return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + }; + + ContributorsGraph.prototype.draw_y_axis = function() { + return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + }; + + ContributorsGraph.prototype.set_data = function(data) { + return this.data = data; + }; + + return ContributorsGraph; + + })(); + + this.ContributorsMasterGraph = (function(superClass) { + extend(ContributorsMasterGraph, superClass); + + function ContributorsMasterGraph(data1) { + this.data = data1; + this.update_content = bind(this.update_content, this); + this.width = $('.content').width() - 70; + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.brush = null; + this.x_max_domain = null; + } + + ContributorsMasterGraph.prototype.process_dates = function(data) { + var dates; + dates = this.get_dates(data); + this.parse_dates(data); + return ContributorsGraph.set_dates(dates); + }; + + ContributorsMasterGraph.prototype.get_dates = function(data) { + return _.pluck(data, 'date'); + }; + + ContributorsMasterGraph.prototype.parse_dates = function(data) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return data.forEach(function(d) { + return d.date = parseDate(d.date); + }); + }; + + ContributorsMasterGraph.prototype.create_scale = function() { + return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsMasterGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsMasterGraph.prototype.create_svg = function() { + return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsMasterGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + return x(d.date); + }).y0(this.height).y1(function(d) { + var ref, ref1, xa; + xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + return y(xa); + }).interpolate("basis"); + }; + + ContributorsMasterGraph.prototype.create_brush = function() { + return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); + }; + + ContributorsMasterGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + }; + + ContributorsMasterGraph.prototype.add_brush = function() { + return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + }; + + ContributorsMasterGraph.prototype.update_content = function() { + ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); + return $("#brush_change").trigger('change'); + }; + + ContributorsMasterGraph.prototype.draw = function() { + this.process_dates(this.data); + this.create_scale(); + this.create_axes(); + ContributorsGraph.init_domain(this.data); + this.x_max_domain = this.x_domain; + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.create_brush(); + this.draw_path(this.data); + this.draw_x_axis(); + this.draw_y_axis(); + return this.add_brush(); + }; + + ContributorsMasterGraph.prototype.redraw = function() { + this.process_dates(this.data); + ContributorsGraph.set_y_domain(this.data); + this.set_y_domain(); + this.svg.select("path").datum(this.data); + this.svg.select("path").attr("d", this.area); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsMasterGraph; + + })(ContributorsGraph); + + this.ContributorsAuthorGraph = (function(superClass) { + extend(ContributorsAuthorGraph, superClass); + + function ContributorsAuthorGraph(data1) { + this.data = data1; + if ($(window).width() < 768) { + this.width = $('.content').width() - 80; + } else { + this.width = ($('.content').width() / 2) - 100; + } + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.list_item = null; + } + + ContributorsAuthorGraph.prototype.create_scale = function() { + return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsAuthorGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsAuthorGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return x(parseDate(d)); + }).y0(this.height).y1((function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this)).interpolate("basis"); + }; + + ContributorsAuthorGraph.prototype.create_svg = function() { + this.list_item = d3.selectAll(".person")[0].pop(); + return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsAuthorGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + }; + + ContributorsAuthorGraph.prototype.draw = function() { + this.create_scale(); + this.create_axes(); + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.draw_path(this.dates); + this.draw_x_axis(); + return this.draw_y_axis(); + }; + + ContributorsAuthorGraph.prototype.redraw = function() { + this.set_domain(); + this.svg.select("path").datum(this.dates); + this.svg.select("path").attr("d", this.area); + this.svg.select(".x.axis").call(this.x_axis); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsAuthorGraph; + + })(ContributorsGraph); + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee deleted file mode 100644 index 834a81af459..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee +++ /dev/null @@ -1,173 +0,0 @@ -#= require d3 - -class @ContributorsGraph - MARGIN: - top: 20 - right: 20 - bottom: 30 - left: 50 - x_domain: null - y_domain: null - dates: [] - @set_x_domain: (data) => - @prototype.x_domain = data - @set_y_domain: (data) => - @prototype.y_domain = [0, d3.max(data, (d) -> - d.commits = d.commits ? d.additions ? d.deletions - )] - @init_x_domain: (data) => - @prototype.x_domain = d3.extent(data, (d) -> - d.date - ) - @init_y_domain: (data) => - @prototype.y_domain = [0, d3.max(data, (d) -> - d.commits = d.commits ? d.additions ? d.deletions - )] - @init_domain: (data) => - @init_x_domain(data) - @init_y_domain(data) - @set_dates: (data) => - @prototype.dates = data - set_x_domain: -> - @x.domain(@x_domain) - set_y_domain: -> - @y.domain(@y_domain) - set_domain: -> - @set_x_domain() - @set_y_domain() - create_scale: (width, height) -> - @x = d3.time.scale().range([0, width]).clamp(true) - @y = d3.scale.linear().range([height, 0]).nice() - draw_x_axis: -> - @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})") - .call(@x_axis) - draw_y_axis: -> - @svg.append("g").attr("class", "y axis").call(@y_axis) - set_data: (data) -> - @data = data - -class @ContributorsMasterGraph extends ContributorsGraph - constructor: (@data) -> - @width = $('.content').width() - 70 - @height = 200 - @x = null - @y = null - @x_axis = null - @y_axis = null - @area = null - @svg = null - @brush = null - @x_max_domain = null - process_dates: (data) -> - dates = @get_dates(data) - @parse_dates(data) - ContributorsGraph.set_dates(dates) - get_dates: (data) -> - _.pluck(data, 'date') - parse_dates: (data) -> - parseDate = d3.time.format("%Y-%m-%d").parse - data.forEach((d) -> - d.date = parseDate(d.date) - ) - create_scale: -> - super @width, @height - create_axes: -> - @x_axis = d3.svg.axis().scale(@x).orient("bottom") - @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) - create_svg: -> - @svg = d3.select("#contributors-master").append("svg") - .attr("width", @width + @MARGIN.left + @MARGIN.right) - .attr("height", @height + @MARGIN.top + @MARGIN.bottom) - .attr("class", "tint-box") - .append("g") - .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") - create_area: (x, y) -> - @area = d3.svg.area().x((d) -> - x(d.date) - ).y0(@height).y1((d) -> - xa = d.commits = d.commits ? d.additions ? d.deletions - y(xa) - ).interpolate("basis") - create_brush: -> - @brush = d3.svg.brush().x(@x).on("brushend", @update_content) - draw_path: (data) -> - @svg.append("path").datum(data).attr("class", "area").attr("d", @area) - add_brush: -> - @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height) - update_content: => - ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent()) - $("#brush_change").trigger('change') - draw: -> - @process_dates(@data) - @create_scale() - @create_axes() - ContributorsGraph.init_domain(@data) - @x_max_domain = @x_domain - @set_domain() - @create_area(@x, @y) - @create_svg() - @create_brush() - @draw_path(@data) - @draw_x_axis() - @draw_y_axis() - @add_brush() - redraw: -> - @process_dates(@data) - ContributorsGraph.set_y_domain(@data) - @set_y_domain() - @svg.select("path").datum(@data) - @svg.select("path").attr("d", @area) - @svg.select(".y.axis").call(@y_axis) - -class @ContributorsAuthorGraph extends ContributorsGraph - constructor: (@data) -> - # Don't split graph size in half for mobile devices. - if $(window).width() < 768 - @width = $('.content').width() - 80 - else - @width = ($('.content').width() / 2) - 100 - @height = 200 - @x = null - @y = null - @x_axis = null - @y_axis = null - @area = null - @svg = null - @list_item = null - create_scale: -> - super @width, @height - create_axes: -> - @x_axis = d3.svg.axis().scale(@x).orient("bottom").ticks(8) - @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) - create_area: (x, y) -> - @area = d3.svg.area().x((d) -> - parseDate = d3.time.format("%Y-%m-%d").parse - x(parseDate(d)) - ).y0(@height).y1((d) => - if @data[d]? then y(@data[d]) else y(0) - ).interpolate("basis") - create_svg: -> - @list_item = d3.selectAll(".person")[0].pop() - @svg = d3.select(@list_item).append("svg") - .attr("width", @width + @MARGIN.left + @MARGIN.right) - .attr("height", @height + @MARGIN.top + @MARGIN.bottom) - .attr("class", "spark") - .append("g") - .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") - draw_path: (data) -> - @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area) - draw: -> - @create_scale() - @create_axes() - @set_domain() - @create_area(@x, @y) - @create_svg() - @draw_path(@dates) - @draw_x_axis() - @draw_y_axis() - redraw: -> - @set_domain() - @svg.select("path").datum(@dates) - @svg.select("path").attr("d", @area) - @svg.select(".x.axis").call(@x_axis) - @svg.select(".y.axis").call(@y_axis) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js new file mode 100644 index 00000000000..0d240bed8b6 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -0,0 +1,135 @@ +(function() { + window.ContributorsStatGraphUtil = { + parse_log: function(log) { + var by_author, by_email, data, entry, i, len, total; + total = {}; + by_author = {}; + by_email = {}; + for (i = 0, len = log.length; i < len; i++) { + entry = log[i]; + if (total[entry.date] == null) { + this.add_date(entry.date, total); + } + data = by_author[entry.author_name] || by_email[entry.author_email]; + if (data == null) { + data = this.add_author(entry, by_author, by_email); + } + if (!data[entry.date]) { + this.add_date(entry.date, data); + } + this.store_data(entry, total[entry.date], data[entry.date]); + } + total = _.toArray(total); + by_author = _.toArray(by_author); + return { + total: total, + by_author: by_author + }; + }, + add_date: function(date, collection) { + collection[date] = {}; + return collection[date].date = date; + }, + add_author: function(author, by_author, by_email) { + var data; + data = {}; + data.author_name = author.author_name; + data.author_email = author.author_email; + by_author[author.author_name] = data; + return by_email[author.author_email] = data; + }, + store_data: function(entry, total, by_author) { + this.store_commits(total, by_author); + this.store_additions(entry, total, by_author); + return this.store_deletions(entry, total, by_author); + }, + store_commits: function(total, by_author) { + this.add(total, "commits", 1); + return this.add(by_author, "commits", 1); + }, + add: function(collection, field, value) { + if (collection[field] == null) { + collection[field] = 0; + } + return collection[field] += value; + }, + store_additions: function(entry, total, by_author) { + if (entry.additions == null) { + entry.additions = 0; + } + this.add(total, "additions", entry.additions); + return this.add(by_author, "additions", entry.additions); + }, + store_deletions: function(entry, total, by_author) { + if (entry.deletions == null) { + entry.deletions = 0; + } + this.add(total, "deletions", entry.deletions); + return this.add(by_author, "deletions", entry.deletions); + }, + get_total_data: function(parsed_log, field) { + var log, total_data; + log = parsed_log.total; + total_data = this.pick_field(log, field); + return _.sortBy(total_data, function(d) { + return d.date; + }); + }, + pick_field: function(log, field) { + var total_data; + total_data = []; + _.each(log, function(d) { + return total_data.push(_.pick(d, [field, 'date'])); + }); + return total_data; + }, + get_author_data: function(parsed_log, field, date_range) { + var author_data, log; + if (date_range == null) { + date_range = null; + } + log = parsed_log.by_author; + author_data = []; + _.each(log, (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this)); + return _.sortBy(author_data, function(d) { + return d[field]; + }).reverse(); + }, + parse_log_entry: function(log_entry, field, date_range) { + var parsed_entry; + parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; + parsed_entry.author_email = log_entry.author_email; + parsed_entry.dates = {}; + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return parsed_entry.deletions += value.deletions; + } + }; + })(this)); + return parsed_entry; + }, + in_range: function(date, date_range) { + var ref; + if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { + return true; + } else { + return false; + } + } + }; + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee deleted file mode 100644 index 31617c88b4a..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee +++ /dev/null @@ -1,98 +0,0 @@ -window.ContributorsStatGraphUtil = - parse_log: (log) -> - total = {} - by_author = {} - by_email = {} - for entry in log - @add_date(entry.date, total) unless total[entry.date]? - - data = by_author[entry.author_name] || by_email[entry.author_email] - data ?= @add_author(entry, by_author, by_email) - - @add_date(entry.date, data) unless data[entry.date] - @store_data(entry, total[entry.date], data[entry.date]) - total = _.toArray(total) - by_author = _.toArray(by_author) - total: total, by_author: by_author - - add_date: (date, collection) -> - collection[date] = {} - collection[date].date = date - - add_author: (author, by_author, by_email) -> - data = {} - data.author_name = author.author_name - data.author_email = author.author_email - by_author[author.author_name] = data - by_email[author.author_email] = data - - store_data: (entry, total, by_author) -> - @store_commits(total, by_author) - @store_additions(entry, total, by_author) - @store_deletions(entry, total, by_author) - - store_commits: (total, by_author) -> - @add(total, "commits", 1) - @add(by_author, "commits", 1) - - add: (collection, field, value) -> - collection[field] ?= 0 - collection[field] += value - - store_additions: (entry, total, by_author) -> - entry.additions ?= 0 - @add(total, "additions", entry.additions) - @add(by_author, "additions", entry.additions) - - store_deletions: (entry, total, by_author) -> - entry.deletions ?= 0 - @add(total, "deletions", entry.deletions) - @add(by_author, "deletions", entry.deletions) - - get_total_data: (parsed_log, field) -> - log = parsed_log.total - total_data = @pick_field(log, field) - _.sortBy(total_data, (d) -> - d.date - ) - pick_field: (log, field) -> - total_data = [] - _.each(log, (d) -> - total_data.push(_.pick(d, [field, 'date'])) - ) - total_data - - get_author_data: (parsed_log, field, date_range = null) -> - log = parsed_log.by_author - author_data = [] - - _.each(log, (log_entry) => - parsed_log_entry = @parse_log_entry(log_entry, field, date_range) - if not _.isEmpty(parsed_log_entry.dates) - author_data.push(parsed_log_entry) - ) - - _.sortBy(author_data, (d) -> - d[field] - ).reverse() - - parse_log_entry: (log_entry, field, date_range) -> - parsed_entry = {} - parsed_entry.author_name = log_entry.author_name - parsed_entry.author_email = log_entry.author_email - parsed_entry.dates = {} - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0 - _.each(_.omit(log_entry, 'author_name', 'author_email'), (value, key) => - if @in_range(value.date, date_range) - parsed_entry.dates[value.date] = value[field] - parsed_entry.commits += value.commits - parsed_entry.additions += value.additions - parsed_entry.deletions += value.deletions - ) - return parsed_entry - - in_range: (date, date_range) -> - if date_range is null || date_range[0] <= new Date(date) <= date_range[1] - true - else - false diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js new file mode 100644 index 00000000000..c28ce86d7af --- /dev/null +++ b/app/assets/javascripts/group_avatar.js @@ -0,0 +1,21 @@ +(function() { + this.GroupAvatar = (function() { + function GroupAvatar() { + $('.js-choose-group-avatar-button').bind("click", function() { + var form; + form = $(this).closest("form"); + return form.find(".js-group-avatar-input").click(); + }); + $('.js-group-avatar-input').bind("change", function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find(".js-avatar-filename").text(filename); + }); + } + + return GroupAvatar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/group_avatar.js.coffee b/app/assets/javascripts/group_avatar.js.coffee deleted file mode 100644 index 0825fd3ce52..00000000000 --- a/app/assets/javascripts/group_avatar.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -class @GroupAvatar - constructor: -> - $('.js-choose-group-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-group-avatar-input").click() - $('.js-group-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js new file mode 100644 index 00000000000..4382dd6860f --- /dev/null +++ b/app/assets/javascripts/groups.js @@ -0,0 +1,13 @@ +(function() { + this.GroupMembers = (function() { + function GroupMembers() { + $('li.group_member').bind('ajax:success', function() { + return $(this).fadeOut(); + }); + } + + return GroupMembers; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/groups.js.coffee b/app/assets/javascripts/groups.js.coffee deleted file mode 100644 index cc905e91ea2..00000000000 --- a/app/assets/javascripts/groups.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @GroupMembers - constructor: -> - $('li.group_member').bind 'ajax:success', -> - $(this).fadeOut() diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js new file mode 100644 index 00000000000..fd5b6dc0ddd --- /dev/null +++ b/app/assets/javascripts/groups_select.js @@ -0,0 +1,67 @@ +(function() { + var slice = [].slice; + + this.GroupsSelect = (function() { + function GroupsSelect() { + $('.ajax-groups-select').each((function(_this) { + return function(i, select) { + var skip_ldap; + skip_ldap = $(select).hasClass('skip_ldap'); + return $(select).select2({ + placeholder: "Search for a group", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return Api.groups(query.term, skip_ldap, function(groups) { + var data; + data = { + results: groups + }; + return query.callback(data); + }); + }, + initSelection: function(element, callback) { + var id; + id = $(element).val(); + if (id !== "") { + return Api.group(id, callback); + } + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-groups-dropdown", + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } + + GroupsSelect.prototype.formatResult = function(group) { + var avatar; + if (group.avatar_url) { + avatar = group.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "<div class='group-result'> <div class='group-name'>" + group.name + "</div> <div class='group-path'>" + group.path + "</div> </div>"; + }; + + GroupsSelect.prototype.formatSelection = function(group) { + return group.name; + }; + + return GroupsSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/groups_select.js.coffee b/app/assets/javascripts/groups_select.js.coffee deleted file mode 100644 index 1084e2a17d1..00000000000 --- a/app/assets/javascripts/groups_select.js.coffee +++ /dev/null @@ -1,41 +0,0 @@ -class @GroupsSelect - constructor: -> - $('.ajax-groups-select').each (i, select) => - skip_ldap = $(select).hasClass('skip_ldap') - - $(select).select2 - placeholder: "Search for a group" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.groups query.term, skip_ldap, (groups) -> - data = { results: groups } - query.callback(data) - - initSelection: (element, callback) -> - id = $(element).val() - if id isnt "" - Api.group(id, callback) - - - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-groups-dropdown" - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - formatResult: (group) -> - if group.avatar_url - avatar = group.avatar_url - else - avatar = gon.default_avatar_url - - "<div class='group-result'> - <div class='group-name'>#{group.name}</div> - <div class='group-path'>#{group.path}</div> - </div>" - - formatSelection: (group) -> - group.name diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js new file mode 100644 index 00000000000..55b6f132bab --- /dev/null +++ b/app/assets/javascripts/importer_status.js @@ -0,0 +1,69 @@ +(function() { + this.ImporterStatus = (function() { + function ImporterStatus(jobs_url, import_url) { + this.jobs_url = jobs_url; + this.import_url = import_url; + this.initStatusPage(); + this.setAutoUpdate(); + } + + ImporterStatus.prototype.initStatusPage = function() { + $('.js-add-to-import').off('click').on('click', (function(_this) { + return function(e) { + var $btn, $namespace_input, $target_field, $tr, id, new_namespace; + $btn = $(e.currentTarget); + $tr = $btn.closest('tr'); + $target_field = $tr.find('.import-target'); + $namespace_input = $target_field.find('input'); + id = $tr.attr('id').replace('repo_', ''); + new_namespace = null; + if ($namespace_input.length > 0) { + new_namespace = $namespace_input.prop('value'); + $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name'))); + } + $btn.disable().addClass('is-loading'); + return $.post(_this.import_url, { + repo_id: id, + new_namespace: new_namespace + }, { + dataType: 'script' + }); + }; + })(this)); + return $('.js-import-all').off('click').on('click', function(e) { + var $btn; + $btn = $(this); + $btn.disable().addClass('is-loading'); + return $('.js-add-to-import').each(function() { + return $(this).trigger('click'); + }); + }); + }; + + ImporterStatus.prototype.setAutoUpdate = function() { + return setInterval(((function(_this) { + return function() { + return $.get(_this.jobs_url, function(data) { + return $.each(data, function(i, job) { + var job_item, status_field; + job_item = $("#project_" + job.id); + status_field = job_item.find(".job-status"); + if (job.import_status === 'finished') { + job_item.removeClass("active").addClass("success"); + return status_field.html('<span><i class="fa fa-check"></i> done</span>'); + } else if (job.import_status === 'started') { + return status_field.html("<i class='fa fa-spinner fa-spin'></i> started"); + } else { + return status_field.html(job.import_status); + } + }); + }); + }; + })(this)), 4000); + }; + + return ImporterStatus; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee deleted file mode 100644 index eb046eb2eff..00000000000 --- a/app/assets/javascripts/importer_status.js.coffee +++ /dev/null @@ -1,53 +0,0 @@ -class @ImporterStatus - constructor: (@jobs_url, @import_url) -> - this.initStatusPage() - this.setAutoUpdate() - - initStatusPage: -> - $('.js-add-to-import') - .off 'click' - .on 'click', (e) => - $btn = $(e.currentTarget) - $tr = $btn.closest('tr') - $target_field = $tr.find('.import-target') - $namespace_input = $target_field.find('input') - id = $tr.attr('id').replace('repo_', '') - new_namespace = null - - if $namespace_input.length > 0 - new_namespace = $namespace_input.prop('value') - $target_field.empty().append("#{new_namespace}/#{$target_field.data('project_name')}") - - $btn - .disable() - .addClass 'is-loading' - - $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' - - $('.js-import-all') - .off 'click' - .on 'click', (e) -> - $btn = $(@) - $btn - .disable() - .addClass 'is-loading' - - $('.js-add-to-import').each -> - $(this).trigger('click') - - setAutoUpdate: -> - setInterval (=> - $.get @jobs_url, (data) => - $.each data, (i, job) => - job_item = $("#project_" + job.id) - status_field = job_item.find(".job-status") - - if job.import_status == 'finished' - job_item.removeClass("active").addClass("success") - status_field.html('<span><i class="fa fa-check"></i> done</span>') - else if job.import_status == 'started' - status_field.html("<i class='fa fa-spinner fa-spin'></i> started") - else - status_field.html(job.import_status) - - ), 4000 diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js new file mode 100644 index 00000000000..f27f1bad1f7 --- /dev/null +++ b/app/assets/javascripts/issuable.js @@ -0,0 +1,89 @@ +(function() { + var issuable_created; + + issuable_created = false; + + this.Issuable = { + init: function() { + if (!issuable_created) { + issuable_created = true; + Issuable.initTemplates(); + Issuable.initSearch(); + Issuable.initChecks(); + return Issuable.initLabelFilterRemove(); + } + }, + initTemplates: function() { + return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); + }, + initSearch: function() { + this.timer = null; + return $('#issue_search').off('keyup').on('keyup', function() { + clearTimeout(this.timer); + return this.timer = setTimeout(function() { + var $form, $input, $search; + $search = $('#issue_search'); + $form = $('.js-filter-form'); + $input = $("input[name='" + ($search.attr('name')) + "']", $form); + if ($input.length === 0) { + $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>"); + } else { + $input.val($search.val()); + } + if ($search.val() !== '') { + return Issuable.filterResults($form); + } + }, 500); + }); + }, + initLabelFilterRemove: function() { + return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { + var $button; + $button = $(this); + $('input[name="label_name[]"]').filter(function() { + return this.value === $button.data('label'); + }).remove(); + Issuable.filterResults($('.filter-form')); + return $('.js-label-select').trigger('update.label'); + }); + }, + filterResults: (function(_this) { + return function(form) { + var formAction, formData, issuesUrl; + formData = form.serialize(); + formAction = form.attr('action'); + issuesUrl = formAction; + issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); + issuesUrl += formData; + return Turbolinks.visit(issuesUrl); + }; + })(this), + initChecks: function() { + this.issuableBulkActions = $('.bulk-update').data('bulkActions'); + $('.check_all_issues').off('click').on('click', function() { + $('.selected_issue').prop('checked', this.checked); + return Issuable.checkChanged(); + }); + return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); + }, + checkChanged: function() { + var checked_issues, ids; + checked_issues = $('.selected_issue:checked'); + if (checked_issues.length > 0) { + ids = $.map(checked_issues, function(value) { + return $(value).data('id'); + }); + $('#update_issues_ids').val(ids); + $('.issues-other-filters').hide(); + $('.issues_bulk_update').show(); + } else { + $('#update_issues_ids').val([]); + $('.issues_bulk_update').hide(); + $('.issues-other-filters').show(); + this.issuableBulkActions.willUpdateLabels = false; + } + return true; + } + }; + +}).call(this); diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee deleted file mode 100644 index 7f795f8096b..00000000000 --- a/app/assets/javascripts/issuable.js.coffee +++ /dev/null @@ -1,93 +0,0 @@ -issuable_created = false -@Issuable = - init: -> - unless issuable_created - issuable_created = true - Issuable.initTemplates() - Issuable.initSearch() - Issuable.initChecks() - Issuable.initLabelFilterRemove() - - initTemplates: -> - Issuable.labelRow = _.template( - '<% _.each(labels, function(label){ %> - <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> - <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> - <%- label.title %> - </a> - <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> - <i class="fa fa-times"></i> - </button> - </span> - <% }); %>' - ) - - initSearch: -> - @timer = null - $('#issue_search') - .off 'keyup' - .on 'keyup', -> - clearTimeout(@timer) - @timer = setTimeout( -> - $search = $('#issue_search') - $form = $('.js-filter-form') - $input = $("input[name='#{$search.attr('name')}']", $form) - if $input.length is 0 - $form.append "<input type='hidden' name='#{$search.attr('name')}' value='#{_.escape($search.val())}'/>" - else - $input.val $search.val() - Issuable.filterResults $form if $search.val() isnt '' - , 500) - - initLabelFilterRemove: -> - $(document) - .off 'click', '.js-label-filter-remove' - .on 'click', '.js-label-filter-remove', (e) -> - $button = $(@) - - # Remove the label input box - $('input[name="label_name[]"]') - .filter -> @value is $button.data('label') - .remove() - - # Submit the form to get new data - Issuable.filterResults $('.filter-form') - $('.js-label-select').trigger('update.label') - - filterResults: (form) => - formData = form.serialize() - - formAction = form.attr('action') - issuesUrl = formAction - issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") - issuesUrl += formData - - Turbolinks.visit(issuesUrl) - - initChecks: -> - @issuableBulkActions = $('.bulk-update').data('bulkActions') - - $('.check_all_issues').off('click').on('click', -> - $('.selected_issue').prop('checked', @checked) - Issuable.checkChanged() - ) - - $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(@)) - - - checkChanged: -> - checked_issues = $('.selected_issue:checked') - if checked_issues.length > 0 - ids = $.map checked_issues, (value) -> - $(value).data('id') - - $('#update_issues_ids').val ids - $('.issues-other-filters').hide() - $('.issues_bulk_update').show() - else - $('#update_issues_ids').val [] - $('.issues_bulk_update').hide() - $('.issues-other-filters').show() - @issuableBulkActions.willUpdateLabels = false - - return true diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js new file mode 100644 index 00000000000..8147e83ffe8 --- /dev/null +++ b/app/assets/javascripts/issuable_context.js @@ -0,0 +1,69 @@ +(function() { + this.IssuableContext = (function() { + function IssuableContext(currentUser) { + this.initParticipants(); + new UsersSelect(currentUser); + $('select.select2').select2({ + width: 'resolve', + dropdownAutoWidth: true + }); + $(".issuable-sidebar .inline-update").on("change", "select", function() { + return $(this).submit(); + }); + $(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() { + return $(this).submit(); + }); + $(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) { + return e.preventDefault(); + }); + $(document).off('click', '.edit-link').on('click', '.edit-link', function(e) { + var $block, $selectbox; + e.preventDefault(); + $block = $(this).parents('.block'); + $selectbox = $block.find('.selectbox'); + if ($selectbox.is(':visible')) { + $selectbox.hide(); + $block.find('.value').show(); + } else { + $selectbox.show(); + $block.find('.value').hide(); + } + if ($selectbox.is(':visible')) { + return setTimeout(function() { + return $block.find('.dropdown-menu-toggle').trigger('click'); + }, 0); + } + }); + $(".right-sidebar").niceScroll(); + } + + IssuableContext.prototype.initParticipants = function() { + var _this; + _this = this; + $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); + return $(".js-participants-author").each(function(i) { + if (i >= _this.PARTICIPANTS_ROW_COUNT) { + return $(this).addClass("js-participants-hidden").hide(); + } + }); + }; + + IssuableContext.prototype.toggleHiddenParticipants = function(e) { + var currentText, lessText, originalText; + e.preventDefault(); + currentText = $(this).text().trim(); + lessText = $(this).data("less-text"); + originalText = $(this).data("original-text"); + if (currentText === originalText) { + $(this).text(lessText); + } else { + $(this).text(originalText); + } + return $(".js-participants-hidden").toggle(); + }; + + return IssuableContext; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee deleted file mode 100644 index 3c491ebfc4c..00000000000 --- a/app/assets/javascripts/issuable_context.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -class @IssuableContext - constructor: (currentUser) -> - @initParticipants() - new UsersSelect(currentUser) - $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) - - $(".issuable-sidebar .inline-update").on "change", "select", -> - $(this).submit() - $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> - $(this).submit() - - $(document) - .off 'click', '.issuable-sidebar .dropdown-content a' - .on 'click', '.issuable-sidebar .dropdown-content a', (e) -> - e.preventDefault() - - $(document) - .off 'click', '.edit-link' - .on 'click', '.edit-link', (e) -> - e.preventDefault() - - $block = $(@).parents('.block') - $selectbox = $block.find('.selectbox') - if $selectbox.is(':visible') - $selectbox.hide() - $block.find('.value').show() - else - $selectbox.show() - $block.find('.value').hide() - - if $selectbox.is(':visible') - setTimeout -> - $block.find('.dropdown-menu-toggle').trigger 'click' - , 0 - - $(".right-sidebar").niceScroll() - - initParticipants: -> - _this = @ - $(document).on "click", ".js-participants-more", @toggleHiddenParticipants - - $(".js-participants-author").each (i) -> - if i >= _this.PARTICIPANTS_ROW_COUNT - $(@) - .addClass "js-participants-hidden" - .hide() - - toggleHiddenParticipants: (e) -> - e.preventDefault() - - currentText = $(this).text().trim() - lessText = $(this).data("less-text") - originalText = $(this).data("original-text") - - if currentText is originalText - $(this).text(lessText) - else - $(this).text(originalText) - - $(".js-participants-hidden").toggle() diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js new file mode 100644 index 00000000000..297d4f029f0 --- /dev/null +++ b/app/assets/javascripts/issuable_form.js @@ -0,0 +1,136 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.IssuableForm = (function() { + IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; + + IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; + + function IssuableForm(form) { + var $issuableDueDate; + this.form = form; + this.toggleWip = bind(this.toggleWip, this); + this.renderWipExplanation = bind(this.renderWipExplanation, this); + this.resetAutosave = bind(this.resetAutosave, this); + this.handleSubmit = bind(this.handleSubmit, this); + GitLab.GfmAutoComplete.setup(); + new UsersSelect(); + new ZenMode(); + this.titleField = this.form.find("input[name*='[title]']"); + this.descriptionField = this.form.find("textarea[name*='[description]']"); + this.issueMoveField = this.form.find("#move_to_project_id"); + if (!(this.titleField.length && this.descriptionField.length)) { + return; + } + this.initAutosave(); + this.form.on("submit", this.handleSubmit); + this.form.on("click", ".btn-cancel", this.resetAutosave); + this.initWip(); + this.initMoveDropdown(); + $issuableDueDate = $('#issuable-due-date'); + if ($issuableDueDate.length) { + $('.datepicker').datepicker({ + dateFormat: 'yy-mm-dd', + onSelect: function(dateText, inst) { + return $issuableDueDate.val(dateText); + } + }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())); + } + } + + IssuableForm.prototype.initAutosave = function() { + new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]); + return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]); + }; + + IssuableForm.prototype.handleSubmit = function() { + var ref, ref1; + if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) { + if (!confirm(this.issueMoveConfirmMsg)) { + return false; + } + } + return this.resetAutosave(); + }; + + IssuableForm.prototype.resetAutosave = function() { + this.titleField.data("autosave").reset(); + return this.descriptionField.data("autosave").reset(); + }; + + IssuableForm.prototype.initWip = function() { + this.$wipExplanation = this.form.find(".js-wip-explanation"); + this.$noWipExplanation = this.form.find(".js-no-wip-explanation"); + if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { + return; + } + this.form.on("click", ".js-toggle-wip", this.toggleWip); + this.titleField.on("keyup blur", this.renderWipExplanation); + return this.renderWipExplanation(); + }; + + IssuableForm.prototype.workInProgress = function() { + return this.wipRegex.test(this.titleField.val()); + }; + + IssuableForm.prototype.renderWipExplanation = function() { + if (this.workInProgress()) { + this.$wipExplanation.show(); + return this.$noWipExplanation.hide(); + } else { + this.$wipExplanation.hide(); + return this.$noWipExplanation.show(); + } + }; + + IssuableForm.prototype.toggleWip = function(event) { + event.preventDefault(); + if (this.workInProgress()) { + this.removeWip(); + } else { + this.addWip(); + } + return this.renderWipExplanation(); + }; + + IssuableForm.prototype.removeWip = function() { + return this.titleField.val(this.titleField.val().replace(this.wipRegex, "")); + }; + + IssuableForm.prototype.addWip = function() { + return this.titleField.val("WIP: " + (this.titleField.val())); + }; + + IssuableForm.prototype.initMoveDropdown = function() { + var $moveDropdown; + $moveDropdown = $('.js-move-dropdown'); + if ($moveDropdown.length) { + return $('.js-move-dropdown').select2({ + ajax: { + url: $moveDropdown.data('projects-url'), + results: function(data) { + return { + results: data + }; + }, + data: function(query) { + return { + search: query + }; + } + }, + formatResult: function(project) { + return project.name_with_namespace; + }, + formatSelection: function(project) { + return project.name_with_namespace; + } + }); + } + }; + + return IssuableForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee deleted file mode 100644 index 5b7a4831dfc..00000000000 --- a/app/assets/javascripts/issuable_form.js.coffee +++ /dev/null @@ -1,112 +0,0 @@ -class @IssuableForm - issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?' - wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i - - constructor: (@form) -> - GitLab.GfmAutoComplete.setup() - new UsersSelect() - new ZenMode() - - @titleField = @form.find("input[name*='[title]']") - @descriptionField = @form.find("textarea[name*='[description]']") - @issueMoveField = @form.find("#move_to_project_id") - - return unless @titleField.length && @descriptionField.length - - @initAutosave() - - @form.on "submit", @handleSubmit - @form.on "click", ".btn-cancel", @resetAutosave - - @initWip() - @initMoveDropdown() - - $issuableDueDate = $('#issuable-due-date') - - if $issuableDueDate.length - $('.datepicker').datepicker( - dateFormat: 'yy-mm-dd', - onSelect: (dateText, inst) -> - $issuableDueDate.val dateText - ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()) - - initAutosave: -> - new Autosave @titleField, [ - document.location.pathname, - document.location.search, - "title" - ] - - new Autosave @descriptionField, [ - document.location.pathname, - document.location.search, - "description" - ] - - handleSubmit: => - if (parseInt(@issueMoveField?.val()) ? 0) > 0 - return false unless confirm(@issueMoveConfirmMsg) - - @resetAutosave() - - resetAutosave: => - @titleField.data("autosave").reset() - @descriptionField.data("autosave").reset() - - initWip: -> - @$wipExplanation = @form.find(".js-wip-explanation") - @$noWipExplanation = @form.find(".js-no-wip-explanation") - return unless @$wipExplanation.length and @$noWipExplanation.length - - @form.on "click", ".js-toggle-wip", @toggleWip - - @titleField.on "keyup blur", @renderWipExplanation - - @renderWipExplanation() - - workInProgress: -> - @wipRegex.test @titleField.val() - - renderWipExplanation: => - if @workInProgress() - @$wipExplanation.show() - @$noWipExplanation.hide() - else - @$wipExplanation.hide() - @$noWipExplanation.show() - - toggleWip: (event) => - event.preventDefault() - - if @workInProgress() - @removeWip() - else - @addWip() - - @renderWipExplanation() - - removeWip: -> - @titleField.val @titleField.val().replace(@wipRegex, "") - - addWip: -> - @titleField.val "WIP: #{@titleField.val()}" - - initMoveDropdown: -> - $moveDropdown = $('.js-move-dropdown') - - if $moveDropdown.length - $('.js-move-dropdown').select2 - ajax: - url: $moveDropdown.data('projects-url') - results: (data) -> - return { - results: data - } - data: (query) -> - { - search: query - } - formatResult: (project) -> - project.name_with_namespace - formatSelection: (project) -> - project.name_with_namespace diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js new file mode 100644 index 00000000000..6838d9d8da1 --- /dev/null +++ b/app/assets/javascripts/issue.js @@ -0,0 +1,154 @@ + +/*= require flash */ + + +/*= require jquery.waitforimages */ + + +/*= require task_list */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Issue = (function() { + function Issue() { + this.submitNoteForm = bind(this.submitNoteForm, this); + this.disableTaskList(); + if ($('a.btn-close').length) { + this.initTaskList(); + this.initIssueBtnEventListeners(); + } + this.initMergeRequests(); + this.initRelatedBranches(); + this.initCanCreateBranch(); + } + + Issue.prototype.initTaskList = function() { + $('.detail-page-description .js-task-list-container').taskList('enable'); + return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); + }; + + Issue.prototype.initIssueBtnEventListeners = function() { + var _this, issueFailMessage; + _this = this; + issueFailMessage = 'Unable to update this issue at this time.'; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, isClose, shouldSubmit, url; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(this); + isClose = $this.hasClass('btn-close'); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit) { + _this.submitNoteForm($this.closest('form')); + } + $this.prop('disabled', true); + url = $this.attr('href'); + return $.ajax({ + type: 'PUT', + url: url, + error: function(jqXHR, textStatus, errorThrown) { + var issueStatus; + issueStatus = isClose ? 'close' : 'open'; + return new Flash(issueFailMessage, 'alert'); + }, + success: function(data, textStatus, jqXHR) { + if ('id' in data) { + $(document).trigger('issuable:change'); + if (isClose) { + $('a.btn-close').addClass('hidden'); + $('a.btn-reopen').removeClass('hidden'); + $('div.status-box-closed').removeClass('hidden'); + $('div.status-box-open').addClass('hidden'); + } else { + $('a.btn-reopen').addClass('hidden'); + $('a.btn-close').removeClass('hidden'); + $('div.status-box-closed').addClass('hidden'); + $('div.status-box-open').removeClass('hidden'); + } + } else { + new Flash(issueFailMessage, 'alert'); + } + return $this.prop('disabled', false); + } + }); + }); + }; + + Issue.prototype.submitNoteForm = function(form) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + return form.submit(); + } + }; + + Issue.prototype.disableTaskList = function() { + $('.detail-page-description .js-task-list-container').taskList('disable'); + return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container'); + }; + + Issue.prototype.updateTaskList = function() { + var patchData; + patchData = {}; + patchData['issue'] = { + 'description': $('.js-task-list-field', this).val() + }; + return $.ajax({ + type: 'PATCH', + url: $('form.js-issuable-update').attr('action'), + data: patchData + }); + }; + + Issue.prototype.initMergeRequests = function() { + var $container; + $container = $('#merge-requests'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load referenced merge requests', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + }; + + Issue.prototype.initRelatedBranches = function() { + var $container; + $container = $('#related-branches'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load related branches', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + }; + + Issue.prototype.initCanCreateBranch = function() { + var $container; + $container = $('div#new-branch'); + if ($container.length === 0) { + return; + } + return $.getJSON($container.data('path')).error(function() { + $container.find('.checking').hide(); + $container.find('.unavailable').show(); + return new Flash('Failed to check if a new branch can be created.', 'alert'); + }).success(function(data) { + if (data.can_create_branch) { + $container.find('.checking').hide(); + $container.find('.available').show(); + return $container.find('a').attr('disabled', false); + } else { + $container.find('.checking').hide(); + return $container.find('.unavailable').show(); + } + }); + }; + + return Issue; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee deleted file mode 100644 index f446aa49cde..00000000000 --- a/app/assets/javascripts/issue.js.coffee +++ /dev/null @@ -1,117 +0,0 @@ -#= require flash -#= require jquery.waitforimages -#= require task_list - -class @Issue - constructor: -> - # Prevent duplicate event bindings - @disableTaskList() - if $('a.btn-close').length - @initTaskList() - @initIssueBtnEventListeners() - - @initMergeRequests() - @initRelatedBranches() - @initCanCreateBranch() - - initTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList - - initIssueBtnEventListeners: -> - _this = @ - issueFailMessage = 'Unable to update this issue at this time.' - $('a.btn-close, a.btn-reopen').on 'click', (e) -> - e.preventDefault() - e.stopImmediatePropagation() - $this = $(this) - isClose = $this.hasClass('btn-close') - shouldSubmit = $this.hasClass('btn-comment') - if shouldSubmit - _this.submitNoteForm($this.closest('form')) - $this.prop('disabled', true) - url = $this.attr('href') - $.ajax - type: 'PUT' - url: url, - error: (jqXHR, textStatus, errorThrown) -> - issueStatus = if isClose then 'close' else 'open' - new Flash(issueFailMessage, 'alert') - success: (data, textStatus, jqXHR) -> - if 'id' of data - $(document).trigger('issuable:change'); - if isClose - $('a.btn-close').addClass('hidden') - $('a.btn-reopen').removeClass('hidden') - $('div.status-box-closed').removeClass('hidden') - $('div.status-box-open').addClass('hidden') - else - $('a.btn-reopen').addClass('hidden') - $('a.btn-close').removeClass('hidden') - $('div.status-box-closed').addClass('hidden') - $('div.status-box-open').removeClass('hidden') - else - new Flash(issueFailMessage, 'alert') - $this.prop('disabled', false) - - submitNoteForm: (form) => - noteText = form.find("textarea.js-note-text").val() - if noteText.trim().length > 0 - form.submit() - - disableTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' - - # TODO (rspeicher): Make the issue description inline-editable like a note so - # that we can re-use its form here - updateTaskList: -> - patchData = {} - patchData['issue'] = {'description': $('.js-task-list-field', this).val()} - - $.ajax - type: 'PATCH' - url: $('form.js-issuable-update').attr('action') - data: patchData - - initMergeRequests: -> - $container = $('#merge-requests') - - $.getJSON($container.data('url')) - .error -> - new Flash('Failed to load referenced merge requests', 'alert') - .success (data) -> - if 'html' of data - $container.html(data.html) - - initRelatedBranches: -> - $container = $('#related-branches') - - $.getJSON($container.data('url')) - .error -> - new Flash('Failed to load related branches', 'alert') - .success (data) -> - if 'html' of data - $container.html(data.html) - - initCanCreateBranch: -> - $container = $('div#new-branch') - - # If the user doesn't have the required permissions the container isn't - # rendered at all. - return if $container.length is 0 - - $.getJSON($container.data('path')) - .error -> - $container.find('.checking').hide() - $container.find('.unavailable').show() - - new Flash('Failed to check if a new branch can be created.', 'alert') - .success (data) -> - if data.can_create_branch - $container.find('.checking').hide() - $container.find('.available').show() - $container.find('a').attr('disabled', false) - else - $container.find('.checking').hide() - $container.find('.unavailable').show() diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js new file mode 100644 index 00000000000..076e3972944 --- /dev/null +++ b/app/assets/javascripts/issue_status_select.js @@ -0,0 +1,35 @@ +(function() { + this.IssueStatusSelect = (function() { + function IssueStatusSelect() { + $('.js-issue-status').each(function(i, el) { + var fieldName; + fieldName = $(el).data("field-name"); + return $(el).glDropdown({ + selectable: true, + fieldName: fieldName, + toggleLabel: (function(_this) { + return function(selected, el, instance) { + var $item, label; + label = 'Author'; + $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); + } + return label; + }; + })(this), + clicked: function(item, $el, e) { + return e.preventDefault(); + }, + id: function(obj, el) { + return $(el).data("id"); + } + }); + }); + } + + return IssueStatusSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee deleted file mode 100644 index ed50e2e698f..00000000000 --- a/app/assets/javascripts/issue_status_select.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -class @IssueStatusSelect - constructor: -> - $('.js-issue-status').each (i, el) -> - fieldName = $(el).data("field-name") - - $(el).glDropdown( - selectable: true - fieldName: fieldName - toggleLabel: (selected, el, instance) => - label = 'Author' - $item = instance.dropdown.find('.is-active') - label = $item.text() if $item.length - label - clicked: (item, $el, e)-> - e.preventDefault() - id: (obj, el) -> - $(el).data("id") - ) diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js new file mode 100644 index 00000000000..98d3358ba92 --- /dev/null +++ b/app/assets/javascripts/issues-bulk-assignment.js @@ -0,0 +1,161 @@ +(function() { + this.IssuableBulkActions = (function() { + function IssuableBulkActions(opts) { + 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('.issues-list .issue'); + this.form.data('bulkActions', this); + this.willUpdateLabels = false; + this.bindEvents(); + Issuable.initChecks(); + } + + IssuableBulkActions.prototype.getElement = function(selector) { + return this.container.find(selector); + }; + + IssuableBulkActions.prototype.bindEvents = function() { + return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); + }; + + IssuableBulkActions.prototype.onFormSubmit = function(e) { + e.preventDefault(); + return this.submit(); + }; + + IssuableBulkActions.prototype.submit = function() { + var _this, xhr; + _this = this; + xhr = $.ajax({ + url: this.form.attr('action'), + method: this.form.attr('method'), + dataType: 'JSON', + data: this.getFormDataAsObject() + }); + xhr.done(function(response, status, xhr) { + return location.reload(); + }); + xhr.fail(function() { + return new Flash("Issue update failed"); + }); + return xhr.always(this.onFormSubmitAlways.bind(this)); + }; + + IssuableBulkActions.prototype.onFormSubmitAlways = function() { + return this.form.find('[type="submit"]').enable(); + }; + + IssuableBulkActions.prototype.getSelectedIssues = function() { + return this.issues.has('.selected_issue:checked'); + }; + + IssuableBulkActions.prototype.getLabelsFromSelection = function() { + var labels; + labels = []; + this.getSelectedIssues().map(function() { + var _labels; + _labels = $(this).data('labels'); + if (_labels) { + return _labels.map(function(labelId) { + if (labels.indexOf(labelId) === -1) { + return labels.push(labelId); + } + }); + } + }); + return labels; + }; + + + /** + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + */ + + IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { + var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; + result = []; + labelsToKeep = []; + ref = this.getElement('.labels-filter .is-indeterminate'); + for (i = 0, len = ref.length; i < len; i++) { + el = ref[i]; + labelsToKeep.push($(el).data('labelId')); + } + ref1 = this.getLabelsFromSelection(); + for (j = 0, len1 = ref1.length; j < len1; j++) { + id = ref1[j]; + if (labelsToKeep.indexOf(id) === -1) { + result.push(id); + } + } + return result; + }; + + + /** + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + */ + + IssuableBulkActions.prototype.getFormDataAsObject = function() { + var formData; + formData = { + update: { + state_event: this.form.find('input[name="update[state_event]"]').val(), + assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), + milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), + issues_ids: this.form.find('input[name="update[issues_ids]"]').val(), + subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), + add_label_ids: [], + remove_label_ids: [] + } + }; + if (this.willUpdateLabels) { + this.getLabelsToApply().map(function(id) { + return formData.update.add_label_ids.push(id); + }); + this.getLabelsToRemove().map(function(id) { + return formData.update.remove_label_ids.push(id); + }); + } + return formData; + }; + + IssuableBulkActions.prototype.getLabelsToApply = function() { + var $labels, labelIds; + labelIds = []; + $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); + $labels.each(function(k, label) { + if (label) { + return labelIds.push(parseInt($(label).val())); + } + }); + return labelIds; + }; + + + /** + * Returns Label IDs that will be removed from issue selection + * @return {Array} Array of labels IDs + */ + + IssuableBulkActions.prototype.getLabelsToRemove = function() { + var indeterminatedLabels, labelsToApply, result; + result = []; + indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); + labelsToApply = this.getLabelsToApply(); + indeterminatedLabels.map(function(id) { + if (labelsToApply.indexOf(id) === -1) { + return result.push(id); + } + }); + return result; + }; + + return IssuableBulkActions; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee deleted file mode 100644 index 3d09ea08e3b..00000000000 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ /dev/null @@ -1,128 +0,0 @@ -class @IssuableBulkActions - constructor: (opts = {}) -> - # Set defaults - { - @container = $('.content') - @form = @getElement('.bulk-update') - @issues = @getElement('.issues-list .issue') - } = opts - - # Save instance - @form.data 'bulkActions', @ - - @willUpdateLabels = false - - @bindEvents() - - # Fixes bulk-assign not working when navigating through pages - Issuable.initChecks(); - - getElement: (selector) -> - @container.find selector - - bindEvents: -> - @form.off('submit').on('submit', @onFormSubmit.bind(@)) - - onFormSubmit: (e) -> - e.preventDefault() - @submit() - - submit: -> - _this = @ - - xhr = $.ajax - url: @form.attr 'action' - method: @form.attr 'method' - dataType: 'JSON', - data: @getFormDataAsObject() - - xhr.done (response, status, xhr) -> - location.reload() - - xhr.fail -> - new Flash("Issue update failed") - - xhr.always @onFormSubmitAlways.bind(@) - - onFormSubmitAlways: -> - @form.find('[type="submit"]').enable() - - getSelectedIssues: -> - @issues.has('.selected_issue:checked') - - getLabelsFromSelection: -> - labels = [] - - @getSelectedIssues().map -> - _labels = $(@).data('labels') - if _labels - _labels.map (labelId) -> - labels.push(labelId) if labels.indexOf(labelId) is -1 - - labels - - ###* - * Will return only labels that were marked previously and the user has unmarked - * @return {Array} Label IDs - ### - getUnmarkedIndeterminedLabels: -> - result = [] - labelsToKeep = [] - - for el in @getElement('.labels-filter .is-indeterminate') - labelsToKeep.push $(el).data('labelId') - - for id in @getLabelsFromSelection() - # Only the ones that we are not going to keep - result.push(id) if labelsToKeep.indexOf(id) is -1 - - result - - ###* - * Simple form serialization, it will return just what we need - * Returns key/value pairs from form data - ### - getFormDataAsObject: -> - formData = - update: - state_event : @form.find('input[name="update[state_event]"]').val() - assignee_id : @form.find('input[name="update[assignee_id]"]').val() - milestone_id : @form.find('input[name="update[milestone_id]"]').val() - issues_ids : @form.find('input[name="update[issues_ids]"]').val() - subscription_event : @form.find('input[name="update[subscription_event]"]').val() - add_label_ids : [] - remove_label_ids : [] - - if @willUpdateLabels - @getLabelsToApply().map (id) -> - formData.update.add_label_ids.push id - - @getLabelsToRemove().map (id) -> - formData.update.remove_label_ids.push id - - formData - - getLabelsToApply: -> - labelIds = [] - $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') - - $labels.each (k, label) -> - labelIds.push parseInt($(label).val()) if label - - labelIds - - ###* - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - ### - getLabelsToRemove: -> - result = [] - indeterminatedLabels = @getUnmarkedIndeterminedLabels() - labelsToApply = @getLabelsToApply() - - indeterminatedLabels.map (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 - result.push(id) if labelsToApply.indexOf(id) is -1 - - result diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js new file mode 100644 index 00000000000..fe071fca67c --- /dev/null +++ b/app/assets/javascripts/labels.js @@ -0,0 +1,44 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Labels = (function() { + function Labels() { + this.setSuggestedColor = bind(this.setSuggestedColor, this); + this.updateColorPreview = bind(this.updateColorPreview, this); + var form; + form = $('.label-form'); + this.cleanBinding(); + this.addBinding(); + this.updateColorPreview(); + } + + Labels.prototype.addBinding = function() { + $(document).on('click', '.suggest-colors a', this.setSuggestedColor); + return $(document).on('input', 'input#label_color', this.updateColorPreview); + }; + + Labels.prototype.cleanBinding = function() { + $(document).off('click', '.suggest-colors a'); + return $(document).off('input', 'input#label_color'); + }; + + Labels.prototype.updateColorPreview = function() { + var previewColor; + previewColor = $('input#label_color').val(); + return $('div.label-color-preview').css('background-color', previewColor); + }; + + Labels.prototype.setSuggestedColor = function(e) { + var color; + color = $(e.currentTarget).data('color'); + $('input#label_color').val(color); + this.updateColorPreview(); + $('.label-form').trigger('keyup'); + return e.preventDefault(); + }; + + return Labels; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/labels.js.coffee b/app/assets/javascripts/labels.js.coffee deleted file mode 100644 index d05bacd7494..00000000000 --- a/app/assets/javascripts/labels.js.coffee +++ /dev/null @@ -1,28 +0,0 @@ -class @Labels - constructor: -> - form = $('.label-form') - @cleanBinding() - @addBinding() - @updateColorPreview() - - addBinding: -> - $(document).on 'click', '.suggest-colors a', @setSuggestedColor - $(document).on 'input', 'input#label_color', @updateColorPreview - - cleanBinding: -> - $(document).off 'click', '.suggest-colors a' - $(document).off 'input', 'input#label_color' - - # Updates the the preview color with the hex-color input - updateColorPreview: => - previewColor = $('input#label_color').val() - $('div.label-color-preview').css('background-color', previewColor) - - # Updates the preview color with a click on a suggested color - setSuggestedColor: (e) => - color = $(e.currentTarget).data('color') - $('input#label_color').val(color) - @updateColorPreview() - # Notify the form, that color has changed - $('.label-form').trigger('keyup') - e.preventDefault() diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js new file mode 100644 index 00000000000..675dd5b7cea --- /dev/null +++ b/app/assets/javascripts/labels_select.js @@ -0,0 +1,377 @@ +(function() { + this.LabelsSelect = (function() { + function LabelsSelect() { + var _this; + _this = this; + $('.js-label-select').each(function(i, dropdown) { + var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo; + $dropdown = $(dropdown); + projectId = $dropdown.data('project-id'); + labelUrl = $dropdown.data('labels'); + issueUpdateURL = $dropdown.data('issueUpdate'); + selectedLabel = $dropdown.data('selected'); + if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { + selectedLabel = selectedLabel.split(','); + } + newLabelField = $('#new_label_name'); + newColorField = $('#new_label_color'); + showNo = $dropdown.data('show-no'); + showAny = $dropdown.data('show-any'); + defaultLabel = $dropdown.data('default-label'); + abilityName = $dropdown.data('ability-name'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + $form = $dropdown.closest('form'); + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); + $value = $block.find('.value'); + $newLabelError = $('.js-label-error'); + $colorPreview = $('.js-dropdown-label-color-preview'); + $newLabelCreateButton = $('.js-new-label-btn'); + $newLabelError.hide(); + $loading = $block.find('.block-loading').fadeOut(); + if (issueUpdateURL != null) { + issueURLSplit = issueUpdateURL.split('/'); + } + if (issueUpdateURL) { + labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); + labelNoneHTMLTemplate = '<span class="no-value">None</span>'; + } + if (newLabelField.length) { + $('.suggest-colors-dropdown a').on("click", function(e) { + e.preventDefault(); + e.stopPropagation(); + newColorField.val($(this).data('color')).trigger('change'); + return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active'); + }); + resetForm = function() { + newLabelField.val('').trigger('change'); + newColorField.val('').trigger('change'); + return $colorPreview.css('background-color', '').parent().removeClass('is-active'); + }; + $('.dropdown-menu-back').on('click', function() { + return resetForm(); + }); + $('.js-cancel-label-btn').on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + resetForm(); + return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); + }); + enableLabelCreateButton = function() { + if (newLabelField.val() !== '' && newColorField.val() !== '') { + $newLabelError.hide(); + return $newLabelCreateButton.enable(); + } else { + return $newLabelCreateButton.disable(); + } + }; + saveLabel = function() { + return Api.newLabel(projectId, { + name: newLabelField.val(), + color: newColorField.val() + }, function(label) { + var errors; + $newLabelCreateButton.enable(); + if (label.message != null) { + errors = _.map(label.message, function(value, key) { + return key + " " + value[0]; + }); + return $newLabelError.html(errors.join("<br/>")).show(); + } else { + return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); + } + }); + }; + newLabelField.on('keyup change', enableLabelCreateButton); + newColorField.on('keyup change', enableLabelCreateButton); + $newLabelCreateButton.disable().on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + return saveLabel(); + }); + } + saveLabelData = function() { + var data, selected; + selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() { + return this.value; + }).get(); + data = {}; + data[abilityName] = {}; + data[abilityName].label_ids = selected; + if (!selected.length) { + data[abilityName].label_ids = ['']; + } + $loading.fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + dataType: 'JSON', + data: data + }).done(function(data) { + var labelCount, template; + $loading.fadeOut(); + $dropdown.trigger('loaded.gl.dropdown'); + $selectbox.hide(); + data.issueURLSplit = issueURLSplit; + labelCount = 0; + if (data.labels.length) { + template = labelHTMLTemplate(data); + labelCount = data.labels.length; + } else { + template = labelNoneHTMLTemplate; + } + $value.removeAttr('style').html(template); + $sidebarCollapsedValue.text(labelCount); + $('.has-tooltip', $value).tooltip({ + container: 'body' + }); + return $value.find('a').each(function(i) { + return setTimeout((function(_this) { + return function() { + return gl.animate.animate($(_this), 'pulse'); + }; + })(this), 200 * i); + }); + }); + }; + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: labelUrl + }).done(function(data) { + data = _.chain(data).groupBy(function(label) { + return label.title; + }).map(function(label) { + var color; + color = _.map(label, function(dup) { + return dup.color; + }); + return { + id: label[0].id, + title: label[0].title, + color: color, + duplicate: color.length > 1 + }; + }).value(); + if ($dropdown.hasClass('js-extra-options')) { + if (showNo) { + data.unshift({ + id: 0, + title: 'No Label' + }); + } + if (showAny) { + data.unshift({ + isAny: true, + title: 'Any Label' + }); + } + if (data.length > 2) { + data.splice(2, 0, 'divider'); + } + } + return callback(data); + }); + }, + renderRow: function(label, instance) { + var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + $li = $('<li>'); + $a = $('<a href="#">'); + selectedClass = []; + removesAll = label.id === 0 || (label.id == null); + if ($dropdown.hasClass('js-filter-bulk-update')) { + indeterminate = instance.indeterminateIds; + active = instance.activeIds; + if (indeterminate.indexOf(label.id) !== -1) { + selectedClass.push('is-indeterminate'); + } + if (active.indexOf(label.id) !== -1) { + i = selectedClass.indexOf('is-indeterminate'); + if (i !== -1) { + selectedClass.splice(i, 1); + } + selectedClass.push('is-active'); + instance.addInput(this.fieldName, label.id); + } + } + if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) { + selectedClass.push('is-active'); + } + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } + if (label.duplicate) { + spacing = 100 / label.color.length; + label.color = label.color.filter(function(color, i) { + return i < 4; + }); + color = _.map(label.color, function(color, i) { + var percentFirst, percentSecond; + percentFirst = Math.floor(spacing * i); + percentSecond = Math.floor(spacing * (i + 1)); + return color + " " + percentFirst + "%," + color + " " + percentSecond + "% "; + }).join(','); + color = "linear-gradient(" + color + ")"; + } else { + if (label.color != null) { + color = label.color[0]; + } + } + if (color) { + colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; + } else { + colorEl = ''; + } + if (label.id) { + selectedClass.push('label-item'); + $a.attr('data-label-id', label.id); + } + $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + return $li.html($a).prop('outerHTML'); + }, + persistWhenHide: $dropdown.data('persistWhenHide'), + search: { + fields: ['title'] + }, + selectable: true, + filterable: true, + toggleLabel: function(selected, el) { + var selected_labels; + selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active'); + if (selected && (selected.title != null)) { + if (selected_labels.length > 1) { + return selected.title + " +" + (selected_labels.length - 1) + " more"; + } else { + return selected.title; + } + } else if (!selected && selected_labels.length !== 0) { + if (selected_labels.length > 1) { + return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more"; + } else if (selected_labels.length === 1) { + return $(selected_labels).text(); + } + } else { + return defaultLabel; + } + }, + fieldName: $dropdown.data('field-name'), + id: function(label) { + if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { + return label.title; + } else { + return label.id; + } + }, + hidden: function() { + var isIssueIndex, isMRIndex, page, selectedLabels; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; + $selectbox.hide(); + $value.removeAttr('style'); + if ($dropdown.hasClass('js-multiselect')) { + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); + Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + $dropdown.closest('form').submit(); + } else { + if (!$dropdown.hasClass('js-filter-bulk-update')) { + saveLabelData(); + } + } + } + if ($dropdown.hasClass('js-filter-bulk-update')) { + if (!this.options.persistWhenHide) { + return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); + } + } + }, + multiSelect: $dropdown.hasClass('js-multiselect'), + clicked: function(label) { + var isIssueIndex, isMRIndex, page; + _this.enableBulkLabelDropdown(); + if ($dropdown.hasClass('js-filter-bulk-update')) { + return; + } + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + if (!$dropdown.hasClass('js-multiselect')) { + selectedLabel = label.title; + return Issuable.filterResults($dropdown.closest('form')); + } + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else { + if ($dropdown.hasClass('js-multiselect')) { + + } else { + return saveLabelData(); + } + } + }, + setIndeterminateIds: function() { + if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + return this.indeterminateIds = _this.getIndeterminateIds(); + } + }, + setActiveIds: function() { + if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + return this.activeIds = _this.getActiveIds(); + } + } + }); + }); + this.bindEvents(); + } + + LabelsSelect.prototype.bindEvents = function() { + return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); + }; + + LabelsSelect.prototype.onSelectCheckboxIssue = function() { + if ($('.selected_issue:checked').length) { + return; + } + $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); + return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); + }; + + LabelsSelect.prototype.getIndeterminateIds = function() { + var label_ids; + label_ids = []; + $('.selected_issue:checked').each(function(i, el) { + var issue_id; + issue_id = $(el).data('id'); + return label_ids.push($("#issue_" + issue_id).data('labels')); + }); + return _.flatten(label_ids); + }; + + LabelsSelect.prototype.getActiveIds = function() { + var label_ids; + label_ids = []; + $('.selected_issue:checked').each(function(i, el) { + var issue_id; + issue_id = $(el).data('id'); + return label_ids.push($("#issue_" + issue_id).data('labels')); + }); + return _.intersection.apply(_, label_ids); + }; + + LabelsSelect.prototype.enableBulkLabelDropdown = function() { + var issuableBulkActions; + if ($('.selected_issue:checked').length) { + issuableBulkActions = $('.bulk-update').data('bulkActions'); + return issuableBulkActions.willUpdateLabels = true; + } + }; + + return LabelsSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee deleted file mode 100644 index 7688609b301..00000000000 --- a/app/assets/javascripts/labels_select.js.coffee +++ /dev/null @@ -1,386 +0,0 @@ -class @LabelsSelect - constructor: -> - _this = @ - - $('.js-label-select').each (i, dropdown) -> - $dropdown = $(dropdown) - projectId = $dropdown.data('project-id') - labelUrl = $dropdown.data('labels') - issueUpdateURL = $dropdown.data('issueUpdate') - selectedLabel = $dropdown.data('selected') - if selectedLabel? and not $dropdown.hasClass 'js-multiselect' - selectedLabel = selectedLabel.split(',') - newLabelField = $('#new_label_name') - newColorField = $('#new_label_color') - showNo = $dropdown.data('show-no') - showAny = $dropdown.data('show-any') - defaultLabel = $dropdown.data('default-label') - abilityName = $dropdown.data('ability-name') - $selectbox = $dropdown.closest('.selectbox') - $block = $selectbox.closest('.block') - $form = $dropdown.closest('form') - $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') - $value = $block.find('.value') - $newLabelError = $('.js-label-error') - $colorPreview = $('.js-dropdown-label-color-preview') - $newLabelCreateButton = $('.js-new-label-btn') - - $newLabelError.hide() - $loading = $block.find('.block-loading').fadeOut() - - issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? - if issueUpdateURL - labelHTMLTemplate = _.template( - '<% _.each(labels, function(label){ %> - <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> - <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> - <%- label.title %> - </span> - </a> - <% }); %>' - ) - labelNoneHTMLTemplate = '<span class="no-value">None</span>' - - if newLabelField.length - - # Suggested colors in the dropdown to chose from pre-chosen colors - $('.suggest-colors-dropdown a').on "click", (e) -> - e.preventDefault() - e.stopPropagation() - newColorField - .val($(this).data('color')) - .trigger('change') - $colorPreview - .css 'background-color', $(this).data('color') - .parent() - .addClass 'is-active' - - # Cancel button takes back to first page - resetForm = -> - newLabelField - .val '' - .trigger 'change' - newColorField - .val '' - .trigger 'change' - $colorPreview - .css 'background-color', '' - .parent() - .removeClass 'is-active' - - $('.dropdown-menu-back').on 'click', -> - resetForm() - - $('.js-cancel-label-btn').on 'click', (e) -> - e.preventDefault() - e.stopPropagation() - resetForm() - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - - # Listen for change and keyup events on label and color field - # This allows us to enable the button when ready - enableLabelCreateButton = -> - if newLabelField.val() isnt '' and newColorField.val() isnt '' - $newLabelError.hide() - $newLabelCreateButton.enable() - else - $newLabelCreateButton.disable() - - saveLabel = -> - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $newLabelCreateButton.enable() - - if label.message? - errors = _.map label.message, (value, key) -> - "#{key} #{value[0]}" - - $newLabelError - .html errors.join("<br/>") - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - - newLabelField.on 'keyup change', enableLabelCreateButton - - newColorField.on 'keyup change', enableLabelCreateButton - - # Send the API call to create the label - $newLabelCreateButton - .disable() - .on 'click', (e) -> - e.preventDefault() - e.stopPropagation() - saveLabel() - - saveLabelData = -> - selected = $dropdown - .closest('.selectbox') - .find("input[name='#{$dropdown.data('field-name')}']") - .map(-> - @value - ).get() - data = {} - data[abilityName] = {} - data[abilityName].label_ids = selected - if not selected.length - data[abilityName].label_ids = [''] - $loading.fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $.ajax( - type: 'PUT' - url: issueUpdateURL - dataType: 'JSON' - data: data - ).done (data) -> - $loading.fadeOut() - $dropdown.trigger('loaded.gl.dropdown') - $selectbox.hide() - data.issueURLSplit = issueURLSplit - labelCount = 0 - if data.labels.length - template = labelHTMLTemplate(data) - labelCount = data.labels.length - else - template = labelNoneHTMLTemplate - $value - .removeAttr('style') - .html(template) - $sidebarCollapsedValue.text(labelCount) - - $('.has-tooltip', $value).tooltip(container: 'body') - - $value - .find('a') - .each((i) -> - setTimeout(=> - gl.animate.animate($(@), 'pulse') - ,200 * i - ) - ) - - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: labelUrl - ).done (data) -> - data = _.chain data - .groupBy (label) -> - label.title - .map (label) -> - color = _.map label, (dup) -> - dup.color - - return { - id: label[0].id - title: label[0].title - color: color - duplicate: color.length > 1 - } - .value() - - if $dropdown.hasClass 'js-extra-options' - if showNo - data.unshift( - id: 0 - title: 'No Label' - ) - - if showAny - data.unshift( - isAny: true - title: 'Any Label' - ) - - if data.length > 2 - data.splice 2, 0, 'divider' - - callback data - - renderRow: (label, instance) -> - $li = $('<li>') - $a = $('<a href="#">') - - selectedClass = [] - removesAll = label.id is 0 or not label.id? - - if $dropdown.hasClass('js-filter-bulk-update') - indeterminate = instance.indeterminateIds - active = instance.activeIds - - if indeterminate.indexOf(label.id) isnt -1 - selectedClass.push 'is-indeterminate' - - if active.indexOf(label.id) isnt -1 - # Remove is-indeterminate class if the item will be marked as active - i = selectedClass.indexOf 'is-indeterminate' - selectedClass.splice i, 1 unless i is -1 - - selectedClass.push 'is-active' - - # Add input manually - instance.addInput @fieldName, label.id - - if $form.find("input[type='hidden']\ - [name='#{$dropdown.data('fieldName')}']\ - [value='#{this.id(label)}']").length - selectedClass.push 'is-active' - - if $dropdown.hasClass('js-multiselect') and removesAll - selectedClass.push 'dropdown-clear-active' - - if label.duplicate - spacing = 100 / label.color.length - - # Reduce the colors to 4 - label.color = label.color.filter (color, i) -> - i < 4 - - color = _.map(label.color, (color, i) -> - percentFirst = Math.floor(spacing * i) - percentSecond = Math.floor(spacing * (i + 1)) - "#{color} #{percentFirst}%,#{color} #{percentSecond}% " - ).join(',') - color = "linear-gradient(#{color})" - else - if label.color? - color = label.color[0] - - if color - colorEl = "<span class='dropdown-label-box' style='background: #{color}'></span>" - 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 - $li.html($a).prop('outerHTML') - persistWhenHide: $dropdown.data('persistWhenHide') - search: - fields: ['title'] - selectable: true - filterable: true - toggleLabel: (selected, el) -> - selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') - - if selected and selected.title? - if selected_labels.length > 1 - "#{selected.title} +#{selected_labels.length - 1} more" - else - selected.title - else if not selected and selected_labels.length isnt 0 - if selected_labels.length > 1 - "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more" - else if selected_labels.length is 1 - $(selected_labels).text() - else - defaultLabel - fieldName: $dropdown.data('field-name') - id: (label) -> - if $dropdown.hasClass("js-filter-submit") and not label.isAny? - label.title - else - label.id - - hidden: -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is 'projects:merge_requests:index' - - $selectbox.hide() - # display:block overrides the hide-collapse rule - $value.removeAttr('style') - if $dropdown.hasClass 'js-multiselect' - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedLabels = $dropdown - .closest('form') - .find("input:hidden[name='#{$dropdown.data('fieldName')}']") - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass('js-filter-submit') - $dropdown.closest('form').submit() - else - if not $dropdown.hasClass 'js-filter-bulk-update' - saveLabelData() - - if $dropdown.hasClass('js-filter-bulk-update') - # If we are persisting state we need the classes - if not @options.persistWhenHide - $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() - - multiSelect: $dropdown.hasClass 'js-multiselect' - clicked: (label) -> - _this.enableBulkLabelDropdown() - - if $dropdown.hasClass('js-filter-bulk-update') - return - - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is 'projects:merge_requests:index' - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - if not $dropdown.hasClass 'js-multiselect' - selectedLabel = label.title - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' - $dropdown.closest('form').submit() - else - if $dropdown.hasClass 'js-multiselect' - return - else - saveLabelData() - - setIndeterminateIds: -> - if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - @indeterminateIds = _this.getIndeterminateIds() - - setActiveIds: -> - if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - @activeIds = _this.getActiveIds() - ) - - @bindEvents() - - bindEvents: -> - $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue - - onSelectCheckboxIssue: -> - return if $('.selected_issue:checked').length - - # Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove() - - # Also restore button text - $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label') - - getIndeterminateIds: -> - label_ids = [] - - $('.selected_issue:checked').each (i, el) -> - issue_id = $(el).data('id') - label_ids.push $("#issue_#{issue_id}").data('labels') - - _.flatten(label_ids) - - getActiveIds: -> - label_ids = [] - - $('.selected_issue:checked').each (i, el) -> - issue_id = $(el).data('id') - label_ids.push $("#issue_#{issue_id}").data('labels') - - _.intersection.apply _, label_ids - - enableBulkLabelDropdown: -> - if $('.selected_issue:checked').length - issuableBulkActions = $('.bulk-update').data('bulkActions') - issuableBulkActions.willUpdateLabels = true diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js new file mode 100644 index 00000000000..ce472f3bcd0 --- /dev/null +++ b/app/assets/javascripts/layout_nav.js @@ -0,0 +1,27 @@ +(function() { + var hideEndFade; + + hideEndFade = function($scrollingTabs) { + return $scrollingTabs.each(function() { + var $this; + $this = $(this); + return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth')); + }); + }; + + $(function() { + hideEndFade($('.scrolling-tabs')); + $(window).off('resize.nav').on('resize.nav', function() { + return hideEndFade($('.scrolling-tabs')); + }); + return $('.scrolling-tabs').on('scroll', function(event) { + var $this, currentPosition, maxPosition; + $this = $(this); + currentPosition = $this.scrollLeft(); + maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); + $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); + return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee deleted file mode 100644 index f639f7f5892..00000000000 --- a/app/assets/javascripts/layout_nav.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -hideEndFade = ($scrollingTabs) -> - $scrollingTabs.each -> - $this = $(@) - - $this - .siblings('.fade-right') - .toggleClass('scrolling', $this.width() < $this.prop('scrollWidth')) - -$ -> - - hideEndFade($('.scrolling-tabs')) - - $(window) - .off 'resize.nav' - .on 'resize.nav', -> - hideEndFade($('.scrolling-tabs')) - - $('.scrolling-tabs').on 'scroll', (event) -> - $this = $(this) - currentPosition = $this.scrollLeft() - maxPosition = $this.prop('scrollWidth') - $this.outerWidth() - - $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0) - $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1) diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js new file mode 100644 index 00000000000..8d5e52286b7 --- /dev/null +++ b/app/assets/javascripts/lib/chart.js @@ -0,0 +1,7 @@ + +/*= require Chart */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/chart.js.coffee b/app/assets/javascripts/lib/chart.js.coffee deleted file mode 100644 index 82217fc5107..00000000000 --- a/app/assets/javascripts/lib/chart.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require Chart diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js new file mode 100644 index 00000000000..8ee81804513 --- /dev/null +++ b/app/assets/javascripts/lib/cropper.js @@ -0,0 +1,7 @@ + +/*= require cropper */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/cropper.js.coffee b/app/assets/javascripts/lib/cropper.js.coffee deleted file mode 100644 index 32536d23fe3..00000000000 --- a/app/assets/javascripts/lib/cropper.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require cropper diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js new file mode 100644 index 00000000000..31e6033e756 --- /dev/null +++ b/app/assets/javascripts/lib/d3.js @@ -0,0 +1,7 @@ + +/*= require d3 */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/d3.js.coffee b/app/assets/javascripts/lib/d3.js.coffee deleted file mode 100644 index 74f0a0bb06a..00000000000 --- a/app/assets/javascripts/lib/d3.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require d3 diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js new file mode 100644 index 00000000000..923c575dcfe --- /dev/null +++ b/app/assets/javascripts/lib/raphael.js @@ -0,0 +1,13 @@ + +/*= require raphael */ + + +/*= require g.raphael */ + + +/*= require g.bar */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/raphael.js.coffee b/app/assets/javascripts/lib/raphael.js.coffee deleted file mode 100644 index ab8e5979b87..00000000000 --- a/app/assets/javascripts/lib/raphael.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -#= require raphael -#= require g.raphael -#= require g.bar diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js new file mode 100644 index 00000000000..d36efdabc93 --- /dev/null +++ b/app/assets/javascripts/lib/utils/animate.js @@ -0,0 +1,49 @@ +(function() { + (function(w) { + if (w.gl == null) { + w.gl = {}; + } + if (gl.animate == null) { + gl.animate = {}; + } + gl.animate.animate = function($el, animation, options, done) { + if ((options != null ? options.cssStart : void 0) != null) { + $el.css(options.cssStart); + } + $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() { + $(this).removeClass(animation + ' animated'); + if (done != null) { + done(); + } + if ((options != null ? options.cssEnd : void 0) != null) { + $el.css(options.cssEnd); + } + }); + }; + gl.animate.animateEach = function($els, animation, time, options, done) { + var dfd; + dfd = $.Deferred(); + if (!$els.length) { + dfd.resolve(); + } + $els.each(function(i) { + setTimeout((function(_this) { + return function() { + var $this; + $this = $(_this); + return gl.animate.animate($this, animation, options, function() { + if (i === $els.length - 1) { + dfd.resolve(); + if (done != null) { + return done(); + } + } + }); + }; + })(this), time * i); + }); + return dfd.promise(); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/animate.js.coffee b/app/assets/javascripts/lib/utils/animate.js.coffee deleted file mode 100644 index ec3b44d6126..00000000000 --- a/app/assets/javascripts/lib/utils/animate.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -((w) -> - if not w.gl? then w.gl = {} - if not gl.animate? then gl.animate = {} - - gl.animate.animate = ($el, animation, options, done) -> - if options?.cssStart? - $el.css(options.cssStart) - $el - .removeClass(animation + ' animated') - .addClass(animation + ' animated') - .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass(animation + ' animated') - if done? - done() - if options?.cssEnd? - $el.css(options.cssEnd) - return - return - - gl.animate.animateEach = ($els, animation, time, options, done) -> - dfd = $.Deferred() - if not $els.length - dfd.resolve() - $els.each((i) -> - setTimeout(=> - $this = $(@) - gl.animate.animate($this, animation, options, => - if i is $els.length - 1 - dfd.resolve() - if done? - done() - ) - ,time * i - ) - return - ) - return dfd.promise() - return -) window
\ No newline at end of file diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js new file mode 100644 index 00000000000..9299d0eabd2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -0,0 +1,60 @@ +(function() { + (function(w) { + var base; + w.gl || (w.gl = {}); + (base = w.gl).utils || (base.utils = {}); + w.gl.utils.isInGroupsPage = function() { + return gl.utils.getPagePath() === 'groups'; + }; + w.gl.utils.isInProjectPage = function() { + return gl.utils.getPagePath() === 'projects'; + }; + w.gl.utils.getProjectSlug = function() { + if (this.isInProjectPage()) { + return $('body').data('project'); + } else { + return null; + } + }; + w.gl.utils.getGroupSlug = function() { + if (this.isInGroupsPage()) { + return $('body').data('group'); + } else { + return null; + } + }; + gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { + return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle'); + }; + gl.utils.preventDisabledButtons = function() { + return $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + }; + gl.utils.getPagePath = function() { + return $('body').data('page').split(':')[0]; + }; + return jQuery.timefor = function(time, suffix, expiredLabel) { + var suffixFromNow, timefor; + if (!time) { + return ''; + } + suffix || (suffix = 'remaining'); + expiredLabel || (expiredLabel = 'Past due'); + jQuery.timeago.settings.allowFuture = true; + suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow; + jQuery.timeago.settings.strings.suffixFromNow = suffix; + timefor = $.timeago(time); + if (timefor.indexOf('ago') > -1) { + timefor = expiredLabel; + } + jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow; + return timefor; + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.coffee b/app/assets/javascripts/lib/utils/common_utils.js.coffee deleted file mode 100644 index d4dd3dc329a..00000000000 --- a/app/assets/javascripts/lib/utils/common_utils.js.coffee +++ /dev/null @@ -1,68 +0,0 @@ -((w) -> - - w.gl or= {} - w.gl.utils or= {} - - w.gl.utils.isInGroupsPage = -> - - return gl.utils.getPagePath() is 'groups' - - - w.gl.utils.isInProjectPage = -> - - return gl.utils.getPagePath() is 'projects' - - - w.gl.utils.getProjectSlug = -> - - return if @isInProjectPage() then $('body').data 'project' else null - - - w.gl.utils.getGroupSlug = -> - - return if @isInGroupsPage() then $('body').data 'group' else null - - - - gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) -> - - $tooltipEl - .tooltip 'destroy' - .attr 'title', newTitle - .tooltip 'fixTitle' - - - gl.utils.preventDisabledButtons = -> - - $('.btn').click (e) -> - if $(this).hasClass 'disabled' - e.preventDefault() - e.stopImmediatePropagation() - return false - - gl.utils.getPagePath = -> - return $('body').data('page').split(':')[0] - - - jQuery.timefor = (time, suffix, expiredLabel) -> - - return '' unless time - - suffix or= 'remaining' - expiredLabel or= 'Past due' - - jQuery.timeago.settings.allowFuture = yes - - { suffixFromNow } = jQuery.timeago.settings.strings - jQuery.timeago.settings.strings.suffixFromNow = suffix - - timefor = $.timeago time - - if timefor.indexOf('ago') > -1 - timefor = expiredLabel - - jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow - - return timefor - -) window diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js new file mode 100644 index 00000000000..e817261f210 --- /dev/null +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -0,0 +1,36 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + w.gl.utils.formatDate = function(datetime) { + return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); + }; + w.gl.utils.getDayName = function(date) { + return this.days[date.getDay()]; + }; + return w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { + if (setTimeago == null) { + setTimeago = true; + } + $timeagoEls.each(function() { + var $el; + $el = $(this); + return $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); + }); + if (setTimeago) { + $timeagoEls.timeago(); + $timeagoEls.tooltip('destroy'); + return $timeagoEls.tooltip({ + template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + }); + } + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee deleted file mode 100644 index 2371e913844..00000000000 --- a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee +++ /dev/null @@ -1,28 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] - - w.gl.utils.formatDate = (datetime) -> - dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') - - w.gl.utils.getDayName = (date) -> - this.days[date.getDay()] - - w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> - $timeagoEls.each( -> - $el = $(@) - $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) - ) - - if setTimeago - $timeagoEls.timeago() - $timeagoEls.tooltip('destroy') - - # Recreate with custom template - $timeagoEls.tooltip( - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - ) - -) window diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js new file mode 100644 index 00000000000..42b6ac0589e --- /dev/null +++ b/app/assets/javascripts/lib/utils/notify.js @@ -0,0 +1,41 @@ +(function() { + (function(w) { + var notificationGranted, notifyMe, notifyPermissions; + notificationGranted = function(message, opts, onclick) { + var notification; + notification = new Notification(message, opts); + setTimeout(function() { + return notification.close(); + }, 8000); + if (onclick) { + return notification.onclick = onclick; + } + }; + notifyPermissions = function() { + if ('Notification' in window) { + return Notification.requestPermission(); + } + }; + notifyMe = function(message, body, icon, onclick) { + var opts; + opts = { + body: body, + icon: icon + }; + if (!('Notification' in window)) { + + } else if (Notification.permission === 'granted') { + return notificationGranted(message, opts, onclick); + } else if (Notification.permission !== 'denied') { + return Notification.requestPermission(function(permission) { + if (permission === 'granted') { + return notificationGranted(message, opts, onclick); + } + }); + } + }; + w.notify = notifyMe; + return w.notifyPermissions = notifyPermissions; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/notify.js.coffee b/app/assets/javascripts/lib/utils/notify.js.coffee deleted file mode 100644 index 9e28353ac34..00000000000 --- a/app/assets/javascripts/lib/utils/notify.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -((w) -> - notificationGranted = (message, opts, onclick) -> - notification = new Notification(message, opts) - - # Hide the notification after X amount of seconds - setTimeout -> - notification.close() - , 8000 - - if onclick - notification.onclick = onclick - - notifyPermissions = -> - if 'Notification' of window - Notification.requestPermission() - - notifyMe = (message, body, icon, onclick) -> - opts = - body: body - icon: icon - # Let's check if the browser supports notifications - if !('Notification' of window) - # do nothing - else if Notification.permission == 'granted' - # If it's okay let's create a notification - notificationGranted message, opts, onclick - else if Notification.permission != 'denied' - Notification.requestPermission (permission) -> - # If the user accepts, let's create a notification - if permission == 'granted' - notificationGranted message, opts, onclick - - w.notify = notifyMe - w.notifyPermissions = notifyPermissions -) window diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js new file mode 100644 index 00000000000..130479642f3 --- /dev/null +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -0,0 +1,112 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).text == null) { + base.text = {}; + } + gl.text.randomString = function() { + return Math.random().toString(36).substring(7); + }; + gl.text.replaceRange = function(s, start, end, substitute) { + return s.substring(0, start) + substitute + s.substring(end); + }; + gl.text.selectedText = function(text, textarea) { + return text.substring(textarea.selectionStart, textarea.selectionEnd); + }; + gl.text.lineBefore = function(text, textarea) { + var split; + split = text.substring(0, textarea.selectionStart).trim().split('\n'); + return split[split.length - 1]; + }; + gl.text.lineAfter = function(text, textarea) { + return text.substring(textarea.selectionEnd).trim().split('\n')[0]; + }; + gl.text.blockTagText = function(text, textArea, blockTag, selected) { + var lineAfter, lineBefore; + lineBefore = this.lineBefore(text, textArea); + lineAfter = this.lineAfter(text, textArea); + if (lineBefore === blockTag && lineAfter === blockTag) { + if (blockTag != null) { + textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); + textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); + } + return selected; + } else { + return blockTag + "\n" + selected + "\n" + blockTag; + } + }; + gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { + var insertText, inserted, selectedSplit, startChar; + selectedSplit = selected.split('\n'); + startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; + if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { + if (blockTag != null) { + insertText = this.blockTagText(text, textArea, blockTag, selected); + } else { + insertText = selectedSplit.map(function(val) { + if (val.indexOf(tag) === 0) { + return "" + (val.replace(tag, '')); + } else { + return "" + tag + val; + } + }).join('\n'); + } + } else { + insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); + } + if (document.queryCommandSupported('insertText')) { + inserted = document.execCommand('insertText', false, insertText); + } + if (!inserted) { + try { + document.execCommand("ms-beginUndoUnit"); + } catch (undefined) {} + textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); + try { + document.execCommand("ms-endUndoUnit"); + } catch (undefined) {} + } + return this.moveCursor(textArea, tag, wrap); + }; + gl.text.moveCursor = function(textArea, tag, wrapped) { + var pos; + if (!textArea.setSelectionRange) { + return; + } + if (textArea.selectionStart === textArea.selectionEnd) { + if (wrapped) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } + return textArea.setSelectionRange(pos, pos); + } + }; + gl.text.updateText = function(textArea, tag, blockTag, wrap) { + var $textArea, oldVal, selected, text; + $textArea = $(textArea); + oldVal = $textArea.val(); + textArea = $textArea.get(0); + text = $textArea.val(); + selected = this.selectedText(text, textArea); + $textArea.focus(); + return this.insertText(textArea, text, tag, blockTag, selected, wrap); + }; + gl.text.init = function(form) { + var self; + self = this; + return $('.js-md', form).off('click').on('click', function() { + var $this; + $this = $(this); + return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); + }); + }; + return gl.text.removeListeners = function(form) { + return $('.js-md', form).off(); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/text_utility.js.coffee b/app/assets/javascripts/lib/utils/text_utility.js.coffee deleted file mode 100644 index 2e1407f8738..00000000000 --- a/app/assets/javascripts/lib/utils/text_utility.js.coffee +++ /dev/null @@ -1,105 +0,0 @@ -((w) -> - w.gl ?= {} - w.gl.text ?= {} - - gl.text.randomString = -> Math.random().toString(36).substring(7) - - gl.text.replaceRange = (s, start, end, substitute) -> - s.substring(0, start) + substitute + s.substring(end); - - gl.text.selectedText = (text, textarea) -> - text.substring(textarea.selectionStart, textarea.selectionEnd) - - gl.text.lineBefore = (text, textarea) -> - split = text.substring(0, textarea.selectionStart).trim().split('\n') - split[split.length - 1] - - gl.text.lineAfter = (text, textarea) -> - text.substring(textarea.selectionEnd).trim().split('\n')[0] - - gl.text.blockTagText = (text, textArea, blockTag, selected) -> - lineBefore = @lineBefore(text, textArea) - lineAfter = @lineAfter(text, textArea) - - if lineBefore is blockTag and lineAfter is blockTag - # To remove the block tag we have to select the line before & after - if blockTag? - textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1) - textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1) - - selected - else - "#{blockTag}\n#{selected}\n#{blockTag}" - - gl.text.insertText = (textArea, text, tag, blockTag, selected, wrap) -> - selectedSplit = selected.split('\n') - startChar = if not wrap and textArea.selectionStart > 0 then '\n' else '' - - if selectedSplit.length > 1 and (not wrap or blockTag?) - if blockTag? - insertText = @blockTagText(text, textArea, blockTag, selected) - else - insertText = selectedSplit.map((val) -> - if val.indexOf(tag) is 0 - "#{val.replace(tag, '')}" - else - "#{tag}#{val}" - ).join('\n') - else - insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}" - - if document.queryCommandSupported('insertText') - inserted = document.execCommand 'insertText', false, insertText - - unless inserted - try - document.execCommand("ms-beginUndoUnit") - - textArea.value = @replaceRange( - text, - textArea.selectionStart, - textArea.selectionEnd, - insertText) - try - document.execCommand("ms-endUndoUnit") - - @moveCursor(textArea, tag, wrap) - - gl.text.moveCursor = (textArea, tag, wrapped) -> - return unless textArea.setSelectionRange - - if textArea.selectionStart is textArea.selectionEnd - if wrapped - pos = textArea.selectionStart - tag.length - else - pos = textArea.selectionStart - - textArea.setSelectionRange pos, pos - - gl.text.updateText = (textArea, tag, blockTag, wrap) -> - $textArea = $(textArea) - oldVal = $textArea.val() - textArea = $textArea.get(0) - text = $textArea.val() - selected = @selectedText(text, textArea) - $textArea.focus() - - @insertText(textArea, text, tag, blockTag, selected, wrap) - - gl.text.init = (form) -> - self = @ - $('.js-md', form) - .off 'click' - .on 'click', -> - $this = $(@) - self.updateText( - $this.closest('.md-area').find('textarea'), - $this.data('md-tag'), - $this.data('md-block'), - not $this.data('md-prepend') - ) - - gl.text.removeListeners = (form) -> - $('.js-md', form).off() - -) window diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js new file mode 100644 index 00000000000..dc30babd645 --- /dev/null +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -0,0 +1,15 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + return w.gl.utils.isObject = function(obj) { + return (obj != null) && (obj.constructor === Object); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/type_utility.js.coffee b/app/assets/javascripts/lib/utils/type_utility.js.coffee deleted file mode 100644 index 957f0d86b36..00000000000 --- a/app/assets/javascripts/lib/utils/type_utility.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - w.gl.utils.isObject = (obj) -> - obj? and (obj.constructor is Object) - -) window diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js new file mode 100644 index 00000000000..fffbfd19745 --- /dev/null +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -0,0 +1,64 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + w.gl.utils.getParameterValues = function(sParam) { + var i, sPageURL, sParameterName, sURLVariables, values; + sPageURL = decodeURIComponent(window.location.search.substring(1)); + sURLVariables = sPageURL.split('&'); + sParameterName = void 0; + values = []; + i = 0; + while (i < sURLVariables.length) { + sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] === sParam) { + values.push(sParameterName[1]); + } + i++; + } + return values; + }; + w.gl.utils.mergeUrlParams = function(params, url) { + var lastChar, newUrl, paramName, paramValue, pattern; + newUrl = decodeURIComponent(url); + for (paramName in params) { + paramValue = params[paramName]; + pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); + if (paramValue == null) { + newUrl = newUrl.replace(pattern, ''); + } else if (url.search(pattern) !== -1) { + newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); + } else { + newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; + } + } + lastChar = newUrl[newUrl.length - 1]; + if (lastChar === '&') { + newUrl = newUrl.slice(0, -1); + } + return newUrl; + }; + return w.gl.utils.removeParamQueryString = function(url, param) { + var urlVariables, variables; + url = decodeURIComponent(url); + urlVariables = url.split('&'); + return ((function() { + var j, len, results; + results = []; + for (j = 0, len = urlVariables.length; j < len; j++) { + variables = urlVariables[j]; + if (variables.indexOf(param) === -1) { + results.push(variables); + } + } + return results; + })()).join('&'); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/url_utility.js.coffee b/app/assets/javascripts/lib/utils/url_utility.js.coffee deleted file mode 100644 index e8085e1c2e4..00000000000 --- a/app/assets/javascripts/lib/utils/url_utility.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - # Returns an array containing the value(s) of the - # of the key passed as an argument - w.gl.utils.getParameterValues = (sParam) -> - sPageURL = decodeURIComponent(window.location.search.substring(1)) - sURLVariables = sPageURL.split('&') - sParameterName = undefined - values = [] - i = 0 - while i < sURLVariables.length - sParameterName = sURLVariables[i].split('=') - if sParameterName[0] is sParam - values.push(sParameterName[1]) - i++ - values - - # # - # @param {Object} params - url keys and value to merge - # @param {String} url - # # - w.gl.utils.mergeUrlParams = (params, url) -> - newUrl = decodeURIComponent(url) - for paramName, paramValue of params - pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" - if not paramValue? - newUrl = newUrl.replace pattern, '' - else if url.search(pattern) isnt -1 - newUrl = newUrl.replace pattern, "$1#{paramValue}$2" - else - newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" - - # Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1] - - if lastChar is '&' - newUrl = newUrl.slice 0, -1 - - newUrl - - # removes parameter query string from url. returns the modified url - w.gl.utils.removeParamQueryString = (url, param) -> - url = decodeURIComponent(url) - urlVariables = url.split('&') - ( - variables for variables in urlVariables when variables.indexOf(param) is -1 - ).join('&') - -) window diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js new file mode 100644 index 00000000000..f145bd3ad74 --- /dev/null +++ b/app/assets/javascripts/line_highlighter.js @@ -0,0 +1,115 @@ + +/*= require jquery.scrollTo */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.LineHighlighter = (function() { + LineHighlighter.prototype.highlightClass = 'hll'; + + LineHighlighter.prototype._hash = ''; + + function LineHighlighter(hash) { + var range; + if (hash == null) { + hash = location.hash; + } + this.setHash = bind(this.setHash, this); + this.highlightLine = bind(this.highlightLine, this); + this.clickHandler = bind(this.clickHandler, this); + this._hash = hash; + this.bindEvents(); + if (hash !== '') { + range = this.hashToRange(hash); + if (range[0]) { + this.highlightRange(range); + $.scrollTo("#L" + range[0], { + offset: -150 + }); + } + } + } + + LineHighlighter.prototype.bindEvents = function() { + $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler); + return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { + return event.preventDefault(); + }); + }; + + LineHighlighter.prototype.clickHandler = function(event) { + var current, lineNumber, range; + event.preventDefault(); + this.clearHighlight(); + lineNumber = $(event.target).closest('a').data('line-number'); + current = this.hashToRange(this._hash); + if (!(current[0] && event.shiftKey)) { + this.setHash(lineNumber); + return this.highlightLine(lineNumber); + } else if (event.shiftKey) { + if (lineNumber < current[0]) { + range = [lineNumber, current[0]]; + } else { + range = [current[0], lineNumber]; + } + this.setHash(range[0], range[1]); + return this.highlightRange(range); + } + }; + + LineHighlighter.prototype.clearHighlight = function() { + return $("." + this.highlightClass).removeClass(this.highlightClass); + }; + + LineHighlighter.prototype.hashToRange = function(hash) { + var first, last, matches; + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); + if (matches && matches.length) { + first = parseInt(matches[1]); + last = matches[2] ? parseInt(matches[2]) : null; + return [first, last]; + } else { + return [null, null]; + } + }; + + LineHighlighter.prototype.highlightLine = function(lineNumber) { + return $("#LC" + lineNumber).addClass(this.highlightClass); + }; + + LineHighlighter.prototype.highlightRange = function(range) { + var i, lineNumber, ref, ref1, results; + if (range[1]) { + results = []; + for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) { + results.push(this.highlightLine(lineNumber)); + } + return results; + } else { + return this.highlightLine(range[0]); + } + }; + + LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { + var hash; + if (lastLineNumber) { + hash = "#L" + firstLineNumber + "-" + lastLineNumber; + } else { + hash = "#L" + firstLineNumber; + } + this._hash = hash; + return this.__setLocationHash__(hash); + }; + + LineHighlighter.prototype.__setLocationHash__ = function(value) { + return history.pushState({ + turbolinks: false, + url: value + }, document.title, value); + }; + + return LineHighlighter; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee deleted file mode 100644 index 2254a3f91ae..00000000000 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ /dev/null @@ -1,148 +0,0 @@ -# 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> -# -class @LineHighlighter - # CSS class applied to highlighted lines - highlightClass: 'hll' - - # Internal copy of location.hash so we're not dependent on `location` in tests - _hash: '' - - # Initialize a LineHighlighter object - # - # hash - String URL hash for dependency injection in tests - constructor: (hash = location.hash) -> - @_hash = hash - - @bindEvents() - - unless hash == '' - range = @hashToRange(hash) - - if range[0] - @highlightRange(range) - - # Scroll to the first highlighted line on initial load - # Offset -50 for the sticky top bar, and another -100 for some context - $.scrollTo("#L#{range[0]}", offset: -150) - - bindEvents: -> - $('#blob-content-holder').on 'mousedown', 'a[data-line-number]', @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. - - $('#blob-content-holder').on 'click', 'a[data-line-number]', (event) -> - event.preventDefault() - - clickHandler: (event) => - event.preventDefault() - - @clearHighlight() - - lineNumber = $(event.target).closest('a').data('line-number') - current = @hashToRange(@_hash) - - unless 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. - @setHash(lineNumber) - @highlightLine(lineNumber) - else if event.shiftKey - if lineNumber < current[0] - range = [lineNumber, current[0]] - else - range = [current[0], lineNumber] - - @setHash(range[0], range[1]) - @highlightRange(range) - - # Unhighlight previously highlighted lines - clearHighlight: -> - $(".#{@highlightClass}").removeClass(@highlightClass) - - # 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 - hashToRange: (hash) -> - matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) - - if matches && matches.length - first = parseInt(matches[1]) - last = if matches[2] then parseInt(matches[2]) else null - - [first, last] - else - [null, null] - - # Highlight a single line - # - # lineNumber - Line number to highlight - highlightLine: (lineNumber) => - $("#LC#{lineNumber}").addClass(@highlightClass) - - # Highlight all lines within a range - # - # range - Array containing the starting and ending line numbers - highlightRange: (range) -> - if range[1] - for lineNumber in [range[0]..range[1]] - @highlightLine(lineNumber) - else - @highlightLine(range[0]) - - # Set the URL hash string - setHash: (firstLineNumber, lastLineNumber) => - if lastLineNumber - hash = "#L#{firstLineNumber}-#{lastLineNumber}" - else - hash = "#L#{firstLineNumber}" - - @_hash = hash - @__setLocationHash__(hash) - - # Make the actual hash change in the browser - # - # This method is stubbed in tests. - __setLocationHash__: (value) -> - # We're using pushState instead of assigning location.hash directly to - # prevent the page from scrolling on the hashchange event - history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js new file mode 100644 index 00000000000..218f24fe908 --- /dev/null +++ b/app/assets/javascripts/logo.js @@ -0,0 +1,54 @@ +(function() { + var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work; + + Turbolinks.enableProgressBar(); + + defaultClass = 'tanuki-shape'; + + pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek']; + + pieceIndex = 0; + + firstPiece = pieces[0]; + + currentTimer = null; + + delay = 150; + + clearHighlights = function() { + return $("." + defaultClass + ".highlight").attr('class', defaultClass); + }; + + start = function() { + clearHighlights(); + pieceIndex = 0; + if (pieces[0] !== firstPiece) { + pieces.reverse(); + } + if (currentTimer) { + clearInterval(currentTimer); + } + return currentTimer = setInterval(work, delay); + }; + + stop = function() { + clearInterval(currentTimer); + return clearHighlights(); + }; + + work = function() { + clearHighlights(); + $(pieces[pieceIndex]).attr('class', defaultClass + " highlight"); + if (pieceIndex === pieces.length - 1) { + pieceIndex = 0; + return pieces.reverse(); + } else { + return pieceIndex++; + } + }; + + $(document).on('page:fetch', start); + + $(document).on('page:change', stop); + +}).call(this); diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee deleted file mode 100644 index dc2590a0355..00000000000 --- a/app/assets/javascripts/logo.js.coffee +++ /dev/null @@ -1,44 +0,0 @@ -Turbolinks.enableProgressBar(); - -defaultClass = 'tanuki-shape' -pieces = [ - 'path#tanuki-right-cheek', - 'path#tanuki-right-eye, path#tanuki-right-ear', - 'path#tanuki-nose', - 'path#tanuki-left-eye, path#tanuki-left-ear', - 'path#tanuki-left-cheek', -] -pieceIndex = 0 -firstPiece = pieces[0] - -currentTimer = null -delay = 150 - -clearHighlights = -> - $(".#{defaultClass}.highlight").attr('class', defaultClass) - -start = -> - clearHighlights() - pieceIndex = 0 - pieces.reverse() unless pieces[0] == firstPiece - clearInterval(currentTimer) if currentTimer - currentTimer = setInterval(work, delay) - -stop = -> - clearInterval(currentTimer) - clearHighlights() - -work = -> - clearHighlights() - $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight") - - # If we hit the last piece, reset the index and then reverse the array to - # get a nice back-and-forth sweeping look - if pieceIndex == pieces.length - 1 - pieceIndex = 0 - pieces.reverse() - else - pieceIndex++ - -$(document).on('page:fetch', start) -$(document).on('page:change', stop) diff --git a/app/assets/javascripts/markdown_preview.js b/app/assets/javascripts/markdown_preview.js new file mode 100644 index 00000000000..18fc7bae09a --- /dev/null +++ b/app/assets/javascripts/markdown_preview.js @@ -0,0 +1,150 @@ +(function() { + var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; + + this.MarkdownPreview = (function() { + function MarkdownPreview() {} + + MarkdownPreview.prototype.referenceThreshold = 10; + + MarkdownPreview.prototype.ajaxCache = {}; + + MarkdownPreview.prototype.showPreview = function(form) { + var mdText, preview; + preview = form.find('.js-md-preview'); + mdText = form.find('textarea.markdown-area').val(); + if (mdText.trim().length === 0) { + preview.text('Nothing to preview.'); + return this.hideReferencedUsers(form); + } else { + preview.text('Loading...'); + return this.renderMarkdown(mdText, (function(_this) { + return function(response) { + preview.html(response.body); + preview.syntaxHighlight(); + return _this.renderReferencedUsers(response.references.users, form); + }; + })(this)); + } + }; + + MarkdownPreview.prototype.renderMarkdown = function(text, success) { + if (!window.markdown_preview_path) { + return; + } + if (text === this.ajaxCache.text) { + return success(this.ajaxCache.response); + } + return $.ajax({ + type: 'POST', + url: window.markdown_preview_path, + data: { + text: text + }, + dataType: 'json', + success: (function(_this) { + return function(response) { + _this.ajaxCache = { + text: text, + response: response + }; + return success(response); + }; + })(this) + }); + }; + + MarkdownPreview.prototype.hideReferencedUsers = function(form) { + var referencedUsers; + referencedUsers = form.find('.referenced-users'); + return referencedUsers.hide(); + }; + + MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { + var referencedUsers; + referencedUsers = form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + return referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + return referencedUsers.hide(); + } + } + }; + + return MarkdownPreview; + + })(); + + markdownPreview = new MarkdownPreview(); + + previewButtonSelector = '.js-md-preview-button'; + + writeButtonSelector = '.js-md-write-button'; + + lastTextareaPreviewed = null; + + $.fn.setupMarkdownPreview = function() { + var $form, form_textarea; + $form = $(this); + form_textarea = $form.find('textarea.markdown-area'); + form_textarea.on('input', function() { + return markdownPreview.hideReferencedUsers($form); + }); + return form_textarea.on('blur', function() { + return markdownPreview.showPreview($form); + }); + }; + + $(document).on('markdown-preview:show', function(e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + return markdownPreview.showPreview($form); + }); + + $(document).on('markdown-preview:hide', function(e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + return $form.find('.md-preview-holder').hide(); + }); + + $(document).on('markdown-preview:toggle', function(e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + return keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + return keyboardEvent.preventDefault(); + } + }); + + $(document).on('click', previewButtonSelector, function(e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + return $(document).triggerHandler('markdown-preview:show', [$form]); + }); + + $(document).on('click', writeButtonSelector, function(e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + return $(document).triggerHandler('markdown-preview:hide', [$form]); + }); + +}).call(this); diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee deleted file mode 100644 index 2a0b9479445..00000000000 --- a/app/assets/javascripts/markdown_preview.js.coffee +++ /dev/null @@ -1,119 +0,0 @@ -# MarkdownPreview -# -# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, -# and showing a warning when more than `x` users are referenced. -# -class @MarkdownPreview - # Minimum number of users referenced before triggering a warning - referenceThreshold: 10 - ajaxCache: {} - - showPreview: (form) -> - preview = form.find('.js-md-preview') - mdText = form.find('textarea.markdown-area').val() - - if mdText.trim().length == 0 - preview.text('Nothing to preview.') - @hideReferencedUsers(form) - else - preview.text('Loading...') - @renderMarkdown mdText, (response) => - preview.html(response.body) - preview.syntaxHighlight() - @renderReferencedUsers(response.references.users, form) - - renderMarkdown: (text, success) -> - return unless window.markdown_preview_path - - return success(@ajaxCache.response) if text == @ajaxCache.text - - $.ajax - type: 'POST' - url: window.markdown_preview_path - data: { text: text } - dataType: 'json' - success: (response) => - @ajaxCache = text: text, response: response - success(response) - - hideReferencedUsers: (form) -> - referencedUsers = form.find('.referenced-users') - referencedUsers.hide() - - renderReferencedUsers: (users, form) -> - referencedUsers = form.find('.referenced-users') - - if referencedUsers.length - if users.length >= @referenceThreshold - referencedUsers.show() - referencedUsers.find('.js-referenced-users-count').text(users.length) - else - referencedUsers.hide() - -markdownPreview = new MarkdownPreview() - -previewButtonSelector = '.js-md-preview-button' -writeButtonSelector = '.js-md-write-button' -lastTextareaPreviewed = null - -$.fn.setupMarkdownPreview = -> - $form = $(this) - - form_textarea = $form.find('textarea.markdown-area') - - form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) - form_textarea.on 'blur', -> markdownPreview.showPreview($form) - -$(document).on 'markdown-preview:show', (e, $form) -> - return unless $form - - 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() - - markdownPreview.showPreview($form) - -$(document).on 'markdown-preview:hide', (e, $form) -> - return unless $form - - 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() - $form.find('.md-preview-holder').hide() - -$(document).on 'markdown-preview:toggle', (e, keyboardEvent) -> - $target = $(keyboardEvent.target) - - if $target.is('textarea.markdown-area') - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]) - keyboardEvent.preventDefault() - else if lastTextareaPreviewed - $target = lastTextareaPreviewed - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]) - keyboardEvent.preventDefault() - -$(document).on 'click', previewButtonSelector, (e) -> - e.preventDefault() - - $form = $(this).closest('form') - - $(document).triggerHandler('markdown-preview:show', [$form]) - -$(document).on 'click', writeButtonSelector, (e) -> - e.preventDefault() - - $form = $(this).closest('form') - - $(document).triggerHandler('markdown-preview:hide', [$form]) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js new file mode 100644 index 00000000000..47e6dd1084d --- /dev/null +++ b/app/assets/javascripts/merge_request.js @@ -0,0 +1,105 @@ + +/*= require jquery.waitforimages */ + + +/*= require task_list */ + + +/*= require merge_request_tabs */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergeRequest = (function() { + function MergeRequest(opts) { + this.opts = opts != null ? opts : {}; + this.submitNoteForm = bind(this.submitNoteForm, this); + this.$el = $('.merge-request'); + this.$('.show-all-commits').on('click', (function(_this) { + return function() { + return _this.showAllCommits(); + }; + })(this)); + this.initTabs(); + this.disableTaskList(); + this.initMRBtnListeners(); + if ($("a.btn-close").length) { + this.initTaskList(); + } + } + + MergeRequest.prototype.$ = function(selector) { + return this.$el.find(selector); + }; + + MergeRequest.prototype.initTabs = function() { + if (this.opts.action !== 'new') { + return new MergeRequestTabs(this.opts); + } else { + return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); + } + }; + + MergeRequest.prototype.showAllCommits = function() { + this.$('.first-commits').remove(); + return this.$('.all-commits').removeClass('hide'); + }; + + MergeRequest.prototype.initTaskList = function() { + $('.detail-page-description .js-task-list-container').taskList('enable'); + return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); + }; + + MergeRequest.prototype.initMRBtnListeners = function() { + var _this; + _this = this; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, shouldSubmit; + $this = $(this); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit && $this.data('submitted')) { + return; + } + if (shouldSubmit) { + if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return _this.submitNoteForm($this.closest('form'), $this); + } + } + }); + }; + + MergeRequest.prototype.submitNoteForm = function(form, $button) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + form.submit(); + $button.data('submitted', true); + return $button.trigger('click'); + } + }; + + MergeRequest.prototype.disableTaskList = function() { + $('.detail-page-description .js-task-list-container').taskList('disable'); + return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container'); + }; + + MergeRequest.prototype.updateTaskList = function() { + var patchData; + patchData = {}; + patchData['merge_request'] = { + 'description': $('.js-task-list-field', this).val() + }; + return $.ajax({ + type: 'PATCH', + url: $('form.js-issuable-update').attr('action'), + data: patchData + }); + }; + + return MergeRequest; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee deleted file mode 100644 index dabfd91cf14..00000000000 --- a/app/assets/javascripts/merge_request.js.coffee +++ /dev/null @@ -1,82 +0,0 @@ -#= require jquery.waitforimages -#= require task_list - -#= require merge_request_tabs - -class @MergeRequest - # Initialize MergeRequest behavior - # - # Options: - # action - String, current controller action - # - constructor: (@opts = {}) -> - this.$el = $('.merge-request') - - this.$('.show-all-commits').on 'click', => - this.showAllCommits() - - @initTabs() - - # Prevent duplicate event bindings - @disableTaskList() - @initMRBtnListeners() - - if $("a.btn-close").length - @initTaskList() - - # Local jQuery finder - $: (selector) -> - this.$el.find(selector) - - initTabs: -> - if @opts.action != 'new' - # `MergeRequests#new` has no tab-persisting or lazy-loading behavior - new MergeRequestTabs(@opts) - else - # Show the first tab (Commits) - $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') - - showAllCommits: -> - this.$('.first-commits').remove() - this.$('.all-commits').removeClass 'hide' - - initTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList - - initMRBtnListeners: -> - _this = @ - $('a.btn-close, a.btn-reopen').on 'click', (e) -> - $this = $(this) - shouldSubmit = $this.hasClass('btn-comment') - if shouldSubmit && $this.data('submitted') - return - if shouldSubmit - if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen') - e.preventDefault() - e.stopImmediatePropagation() - _this.submitNoteForm($this.closest('form'),$this) - - - submitNoteForm: (form, $button) => - noteText = form.find("textarea.js-note-text").val() - if noteText.trim().length > 0 - form.submit() - $button.data('submitted',true) - $button.trigger('click') - - - disableTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' - - # TODO (rspeicher): Make the merge request description inline-editable like a - # note so that we can re-use its form here - updateTaskList: -> - patchData = {} - patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()} - - $.ajax - type: 'PATCH' - url: $('form.js-issuable-update').attr('action') - data: patchData diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js new file mode 100644 index 00000000000..52c2ed61012 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js @@ -0,0 +1,239 @@ + +/*= require jquery.cookie */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergeRequestTabs = (function() { + MergeRequestTabs.prototype.diffsLoaded = false; + + MergeRequestTabs.prototype.buildsLoaded = false; + + MergeRequestTabs.prototype.commitsLoaded = false; + + function MergeRequestTabs(opts) { + this.opts = opts != null ? opts : {}; + this.setCurrentAction = bind(this.setCurrentAction, this); + this.tabShown = bind(this.tabShown, this); + this.showTab = bind(this.showTab, this); + this._location = location; + this.bindEvents(); + this.activateTab(this.opts.action); + } + + MergeRequestTabs.prototype.bindEvents = function() { + $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); + return $(document).on('click', '.js-show-tab', this.showTab); + }; + + MergeRequestTabs.prototype.showTab = function(event) { + event.preventDefault(); + return this.activateTab($(event.target).data('action')); + }; + + MergeRequestTabs.prototype.tabShown = function(event) { + var $target, action, navBarHeight; + $target = $(event.target); + action = $target.data('action'); + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + } else if (action === 'diffs') { + this.loadDiff($target.attr('href')); + if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + navBarHeight = $('.navbar-gitlab').outerHeight(); + $.scrollTo(".merge-request-details .merge-request-tabs", { + offset: -navBarHeight + }); + } else if (action === 'builds') { + this.loadBuilds($target.attr('href')); + this.expandView(); + } else { + this.expandView(); + } + return this.setCurrentAction(action); + }; + + MergeRequestTabs.prototype.scrollToElement = function(container) { + var $el, navBarHeight; + if (window.location.hash) { + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + $el = $(container + " " + window.location.hash + ":not(.match)"); + if ($el.length) { + return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { + offset: -navBarHeight + }); + } + } + }; + + MergeRequestTabs.prototype.activateTab = function(action) { + if (action === 'show') { + action = 'notes'; + } + return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + }; + + MergeRequestTabs.prototype.setCurrentAction = function(action) { + var new_state; + if (action === 'show') { + action = 'notes'; + } + new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, ''); + if (action !== 'notes') { + new_state += "/" + action; + } + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + return new_state; + }; + + MergeRequestTabs.prototype.loadCommits = function(source) { + if (this.commitsLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#commits").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + _this.commitsLoaded = true; + return _this.scrollToElement("#commits"); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.loadDiff = function(source) { + if (this.diffsLoaded) { + return; + } + return this._get({ + url: (source + ".json") + this._location.search, + success: (function(_this) { + return function(data) { + $('#diffs').html(data.html); + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + $('#diffs .diff-file').singleFileDiff(); + if (_this.diffViewType() === 'parallel') { + _this.expandViewContainer(); + } + _this.diffsLoaded = true; + _this.scrollToElement("#diffs"); + _this.highlighSelectedLine(); + _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { + e.preventDefault(); + window.location.hash = $(e.currentTarget).attr('href'); + _this.highlighSelectedLine(); + return _this.scrollToElement("#diffs"); + }); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.highlighSelectedLine = function() { + var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; + $('.hll').removeClass('hll'); + locationHash = window.location.hash; + if (locationHash !== '') { + hashClassString = "." + (locationHash.replace('#', '')); + $diffLine = $(locationHash + ":not(.match)", $('#diffs')); + if (!$diffLine.is('tr')) { + $diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString); + } else { + $diffLine = $diffLine.find('td'); + } + if ($diffLine.length) { + $diffLine.addClass('hll'); + diffLineTop = $diffLine.offset().top; + return navBarHeight = $('.navbar-gitlab').outerHeight(); + } + } + }; + + MergeRequestTabs.prototype.loadBuilds = function(source) { + if (this.buildsLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#builds").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); + _this.buildsLoaded = true; + return _this.scrollToElement("#builds"); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.toggleLoading = function(status) { + return $('.mr-loading-status .loading').toggle(status); + }; + + MergeRequestTabs.prototype._get = function(options) { + var defaults; + defaults = { + beforeSend: (function(_this) { + return function() { + return _this.toggleLoading(true); + }; + })(this), + complete: (function(_this) { + return function() { + return _this.toggleLoading(false); + }; + })(this), + dataType: 'json', + type: 'GET' + }; + options = $.extend({}, defaults, options); + return $.ajax(options); + }; + + MergeRequestTabs.prototype.diffViewType = function() { + return $('.inline-parallel-buttons a.active').data('view-type'); + }; + + MergeRequestTabs.prototype.expandViewContainer = function() { + return $('.container-fluid').removeClass('container-limited'); + }; + + MergeRequestTabs.prototype.shrinkView = function() { + var $gutterIcon; + $gutterIcon = $('.js-sidebar-toggle i:visible'); + return setTimeout(function() { + if ($gutterIcon.is('.fa-angle-double-right')) { + return $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + }; + + MergeRequestTabs.prototype.expandView = function() { + var $gutterIcon; + if ($.cookie('collapsed_gutter') === 'true') { + return; + } + $gutterIcon = $('.js-sidebar-toggle i:visible'); + return setTimeout(function() { + if ($gutterIcon.is('.fa-angle-double-left')) { + return $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + }; + + return MergeRequestTabs; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee deleted file mode 100644 index 86539e0d725..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ /dev/null @@ -1,252 +0,0 @@ -# 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> -# -class @MergeRequestTabs - diffsLoaded: false - buildsLoaded: false - commitsLoaded: false - - constructor: (@opts = {}) -> - # Store the `location` object, allowing for easier stubbing in tests - @_location = location - - @bindEvents() - @activateTab(@opts.action) - - bindEvents: -> - $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown - $(document).on 'click', '.js-show-tab', @showTab - - showTab: (event) => - event.preventDefault() - - @activateTab $(event.target).data('action') - - tabShown: (event) => - $target = $(event.target) - action = $target.data('action') - - if action == 'commits' - @loadCommits($target.attr('href')) - @expandView() - else if action == 'diffs' - @loadDiff($target.attr('href')) - if bp? and bp.getBreakpointSize() isnt 'lg' - @shrinkView() - - navBarHeight = $('.navbar-gitlab').outerHeight() - $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) - else if action == 'builds' - @loadBuilds($target.attr('href')) - @expandView() - else - @expandView() - - @setCurrentAction(action) - - scrollToElement: (container) -> - if window.location.hash - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() - - $el = $("#{container} #{window.location.hash}:not(.match)") - $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length - - # Activate a tab based on the current action - activateTab: (action) -> - action = 'notes' if action == 'show' - $(".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 - setCurrentAction: (action) => - # Normalize action, just to be safe - action = 'notes' if action == 'show' - - # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '') - - # Append the new action if we're on a tab other than 'notes' - unless action == 'notes' - new_state += "/#{action}" - - # Ensure parameters and hash come along for the ride - new_state += @_location.search + @_location.hash - - # Replace the current history state with the new one without breaking - # Turbolinks' history. - # - # See https://github.com/rails/turbolinks/issues/363 - history.replaceState {turbolinks: true, url: new_state}, document.title, new_state - - new_state - - loadCommits: (source) -> - return if @commitsLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#commits").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) - @commitsLoaded = true - @scrollToElement("#commits") - - loadDiff: (source) -> - return if @diffsLoaded - @_get - url: "#{source}.json" + @_location.search - success: (data) => - $('#diffs').html data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) - $('#diffs .js-syntax-highlight').syntaxHighlight() - $('#diffs .diff-file').singleFileDiff() - @expandViewContainer() if @diffViewType() is 'parallel' - @diffsLoaded = true - @scrollToElement("#diffs") - @highlighSelectedLine() - @filesCommentButton = $('.files .diff-file').filesCommentButton() - - $(document) - .off 'click', '.diff-line-num a' - .on 'click', '.diff-line-num a', (e) => - e.preventDefault() - window.location.hash = $(e.currentTarget).attr 'href' - @highlighSelectedLine() - @scrollToElement("#diffs") - - highlighSelectedLine: -> - $('.hll').removeClass 'hll' - locationHash = window.location.hash - - if locationHash isnt '' - hashClassString = ".#{locationHash.replace('#', '')}" - $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) - - if not $diffLine.is 'tr' - $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") - else - $diffLine = $diffLine.find('td') - - if $diffLine.length - $diffLine.addClass 'hll' - diffLineTop = $diffLine.offset().top - navBarHeight = $('.navbar-gitlab').outerHeight() - - loadBuilds: (source) -> - return if @buildsLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#builds").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) - @buildsLoaded = true - @scrollToElement("#builds") - - # Show or hide the loading spinner - # - # status - Boolean, true to show, false to hide - toggleLoading: (status) -> - $('.mr-loading-status .loading').toggle(status) - - _get: (options) -> - defaults = { - beforeSend: => @toggleLoading(true) - complete: => @toggleLoading(false) - dataType: 'json' - type: 'GET' - } - - options = $.extend({}, defaults, options) - - $.ajax(options) - - # Returns diff view type - diffViewType: -> - $('.inline-parallel-buttons a.active').data('view-type') - - expandViewContainer: -> - $('.container-fluid').removeClass('container-limited') - - shrinkView: -> - $gutterIcon = $('.js-sidebar-toggle i:visible') - - # Wait until listeners are set - setTimeout( -> - # Only when sidebar is expanded - if $gutterIcon.is('.fa-angle-double-right') - $gutterIcon.closest('a').trigger('click', [true]) - , 0) - - # Expand the issuable sidebar unless the user explicitly collapsed it - expandView: -> - return if $.cookie('collapsed_gutter') == 'true' - - $gutterIcon = $('.js-sidebar-toggle i:visible') - - # Wait until listeners are set - setTimeout( -> - # Only when sidebar is collapsed - if $gutterIcon.is('.fa-angle-double-left') - $gutterIcon.closest('a').trigger('click', [true]) - , 0) diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js new file mode 100644 index 00000000000..362aaa906d0 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js @@ -0,0 +1,185 @@ +(function() { + var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + this.MergeRequestWidget = (function() { + function MergeRequestWidget(opts) { + this.opts = opts; + $('#modal_merge_info').modal({ + show: false + }); + this.firstCICheck = true; + this.readyForCICheck = false; + this.cancel = false; + clearInterval(this.fetchBuildStatusInterval); + this.clearEventListeners(); + this.addEventListeners(); + this.getCIStatus(false); + this.pollCIStatus(); + notifyPermissions(); + } + + MergeRequestWidget.prototype.clearEventListeners = function() { + return $(document).off('page:change.merge_request'); + }; + + MergeRequestWidget.prototype.cancelPolling = function() { + return this.cancel = true; + }; + + MergeRequestWidget.prototype.addEventListeners = function() { + var allowedPages; + allowedPages = ['show', 'commits', 'builds', 'changes']; + return $(document).on('page:change.merge_request', (function(_this) { + return function() { + var page; + page = $('body').data('page').split(':').last(); + if (allowedPages.indexOf(page) < 0) { + clearInterval(_this.fetchBuildStatusInterval); + _this.cancelPolling(); + return _this.clearEventListeners(); + } + }; + })(this)); + }; + + MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { + if (deleteSourceBranch == null) { + deleteSourceBranch = false; + } + return $.ajax({ + type: 'GET', + url: $('.merge-request').data('url'), + success: (function(_this) { + return function(data) { + var callback, urlSuffix; + if (data.state === "merged") { + urlSuffix = deleteSourceBranch ? '?delete_source=true' : ''; + return window.location.href = window.location.pathname + urlSuffix; + } else if (data.merge_error) { + return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); + } else { + callback = function() { + return merge_request_widget.mergeInProgress(deleteSourceBranch); + }; + return setTimeout(callback, 2000); + } + }; + })(this), + dataType: 'json' + }); + }; + + MergeRequestWidget.prototype.getMergeStatus = function() { + return $.get(this.opts.merge_check_url, function(data) { + return $('.mr-state-widget').replaceWith(data); + }); + }; + + MergeRequestWidget.prototype.ciLabelForStatus = function(status) { + switch (status) { + case 'success': + return 'passed'; + case 'success_with_warnings': + return 'passed with warnings'; + default: + return status; + } + }; + + MergeRequestWidget.prototype.pollCIStatus = function() { + return this.fetchBuildStatusInterval = setInterval(((function(_this) { + return function() { + if (!_this.readyForCICheck) { + return; + } + _this.getCIStatus(true); + return _this.readyForCICheck = false; + }; + })(this)), 10000); + }; + + MergeRequestWidget.prototype.getCIStatus = function(showNotification) { + var _this; + _this = this; + $('.ci-widget-fetching').show(); + return $.getJSON(this.opts.ci_status_url, (function(_this) { + return function(data) { + var message, status, title; + if (_this.cancel) { + return; + } + _this.readyForCICheck = true; + if (data.status === '') { + return; + } + if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { + _this.opts.ci_status = data.status; + _this.showCIStatus(data.status); + if (data.coverage) { + _this.showCICoverage(data.coverage); + } + if (showNotification && !_this.firstCICheck) { + status = _this.ciLabelForStatus(data.status); + if (status === "preparing") { + title = _this.opts.ci_title.preparing; + status = status.charAt(0).toUpperCase() + status.slice(1); + message = _this.opts.ci_message.preparing.replace('{{status}}', status); + } else { + title = _this.opts.ci_title.normal; + message = _this.opts.ci_message.normal.replace('{{status}}', status); + } + title = title.replace('{{status}}', status); + message = message.replace('{{sha}}', data.sha); + message = message.replace('{{title}}', data.title); + notify(title, message, _this.opts.gitlab_icon, function() { + this.close(); + return Turbolinks.visit(_this.opts.builds_path); + }); + } + return _this.firstCICheck = false; + } + }; + })(this)); + }; + + MergeRequestWidget.prototype.showCIStatus = function(state) { + var allowed_states; + if (state == null) { + return; + } + $('.ci_widget').hide(); + allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; + if (indexOf.call(allowed_states, state) >= 0) { + $('.ci_widget.ci-' + state).show(); + switch (state) { + case "failed": + case "canceled": + case "not_found": + return this.setMergeButtonClass('btn-danger'); + case "running": + return this.setMergeButtonClass('btn-warning'); + case "success": + case "success_with_warnings": + return this.setMergeButtonClass('btn-create'); + } + } else { + $('.ci_widget.ci-error').show(); + return this.setMergeButtonClass('btn-danger'); + } + }; + + MergeRequestWidget.prototype.showCICoverage = function(coverage) { + var text; + text = 'Coverage ' + coverage + '%'; + return $('.ci_widget:visible .ci-coverage').text(text); + }; + + MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) { + return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class); + }; + + return MergeRequestWidget; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee deleted file mode 100644 index 779f536d9f0..00000000000 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ /dev/null @@ -1,140 +0,0 @@ -class @MergeRequestWidget - # 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 - # - - constructor: (@opts) -> - $('#modal_merge_info').modal(show: false) - @firstCICheck = true - @readyForCICheck = false - @cancel = false - clearInterval @fetchBuildStatusInterval - - @clearEventListeners() - @addEventListeners() - @getCIStatus(false) - @pollCIStatus() - notifyPermissions() - - clearEventListeners: -> - $(document).off 'page:change.merge_request' - - cancelPolling: -> - @cancel = true - - addEventListeners: -> - allowedPages = ['show', 'commits', 'builds', 'changes'] - $(document).on 'page:change.merge_request', => - page = $('body').data('page').split(':').last() - if allowedPages.indexOf(page) < 0 - clearInterval @fetchBuildStatusInterval - @cancelPolling() - @clearEventListeners() - - mergeInProgress: (deleteSourceBranch = false)-> - $.ajax - type: 'GET' - url: $('.merge-request').data('url') - success: (data) => - if data.state == "merged" - urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' - - window.location.href = window.location.pathname + urlSuffix - else if data.merge_error - $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>") - else - callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) - setTimeout(callback, 2000) - dataType: 'json' - - getMergeStatus: -> - $.get @opts.merge_check_url, (data) -> - $('.mr-state-widget').replaceWith(data) - - ciLabelForStatus: (status) -> - if status is 'success' - 'passed' - else - status - - pollCIStatus: -> - @fetchBuildStatusInterval = setInterval ( => - return if not @readyForCICheck - - @getCIStatus(true) - - @readyForCICheck = false - ), 10000 - - getCIStatus: (showNotification) -> - _this = @ - $('.ci-widget-fetching').show() - - $.getJSON @opts.ci_status_url, (data) => - return if @cancel - @readyForCICheck = true - - if data.status is '' - return - - if @firstCICheck || data.status isnt @opts.ci_status and data.status? - @opts.ci_status = data.status - @showCIStatus data.status - if data.coverage - @showCICoverage data.coverage - - # The first check should only update the UI, a notification - # should only be displayed on status changes - if showNotification and not @firstCICheck - status = @ciLabelForStatus(data.status) - - if status is "preparing" - title = @opts.ci_title.preparing - status = status.charAt(0).toUpperCase() + status.slice(1); - message = @opts.ci_message.preparing.replace('{{status}}', status) - else - title = @opts.ci_title.normal - message = @opts.ci_message.normal.replace('{{status}}', status) - - title = title.replace('{{status}}', status) - message = message.replace('{{sha}}', data.sha) - message = message.replace('{{title}}', data.title) - - notify( - title, - message, - @opts.gitlab_icon, - -> - @close() - Turbolinks.visit _this.opts.builds_path - ) - @firstCICheck = false - - showCIStatus: (state) -> - return if not state? - $('.ci_widget').hide() - allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] - if state in allowed_states - $('.ci_widget.ci-' + state).show() - switch state - when "failed", "canceled", "not_found" - @setMergeButtonClass('btn-danger') - when "running" - @setMergeButtonClass('btn-warning') - when "success" - @setMergeButtonClass('btn-create') - else - $('.ci_widget.ci-error').show() - @setMergeButtonClass('btn-danger') - - showCICoverage: (coverage) -> - text = 'Coverage ' + coverage + '%' - $('.ci_widget:visible .ci-coverage').text(text) - - setMergeButtonClass: (css_class) -> - $('.js-merge-button,.accept-action .dropdown-toggle') - .removeClass('btn-danger btn-warning btn-create') - .addClass(css_class) diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js new file mode 100644 index 00000000000..1fed38661a2 --- /dev/null +++ b/app/assets/javascripts/merged_buttons.js @@ -0,0 +1,45 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergedButtons = (function() { + function MergedButtons() { + this.removeSourceBranch = bind(this.removeSourceBranch, this); + this.$removeBranchWidget = $('.remove_source_branch_widget'); + this.$removeBranchProgress = $('.remove_source_branch_in_progress'); + this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); + this.cleanEventListeners(); + this.initEventListeners(); + } + + MergedButtons.prototype.cleanEventListeners = function() { + $(document).off('click', '.remove_source_branch'); + $(document).off('ajax:success', '.remove_source_branch'); + return $(document).off('ajax:error', '.remove_source_branch'); + }; + + MergedButtons.prototype.initEventListeners = function() { + $(document).on('click', '.remove_source_branch', this.removeSourceBranch); + $(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess); + return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError); + }; + + MergedButtons.prototype.removeSourceBranch = function() { + this.$removeBranchWidget.hide(); + return this.$removeBranchProgress.show(); + }; + + MergedButtons.prototype.removeBranchSuccess = function() { + return location.reload(); + }; + + MergedButtons.prototype.removeBranchError = function() { + this.$removeBranchWidget.hide(); + this.$removeBranchProgress.hide(); + return this.$removeBranchFailed.show(); + }; + + return MergedButtons; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merged_buttons.js.coffee b/app/assets/javascripts/merged_buttons.js.coffee deleted file mode 100644 index 4929295c10b..00000000000 --- a/app/assets/javascripts/merged_buttons.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -class @MergedButtons - constructor: -> - @$removeBranchWidget = $('.remove_source_branch_widget') - @$removeBranchProgress = $('.remove_source_branch_in_progress') - @$removeBranchFailed = $('.remove_source_branch_widget.failed') - - @cleanEventListeners() - @initEventListeners() - - cleanEventListeners: -> - $(document).off 'click', '.remove_source_branch' - $(document).off 'ajax:success', '.remove_source_branch' - $(document).off 'ajax:error', '.remove_source_branch' - - initEventListeners: -> - $(document).on 'click', '.remove_source_branch', @removeSourceBranch - $(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess - $(document).on 'ajax:error', '.remove_source_branch', @removeBranchError - - removeSourceBranch: => - @$removeBranchWidget.hide() - @$removeBranchProgress.show() - - removeBranchSuccess: -> - location.reload() - - removeBranchError: -> - @$removeBranchWidget.hide() - @$removeBranchProgress.hide() - @$removeBranchFailed.show() diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js new file mode 100644 index 00000000000..e8d51da7d58 --- /dev/null +++ b/app/assets/javascripts/milestone.js @@ -0,0 +1,195 @@ +(function() { + this.Milestone = (function() { + Milestone.updateIssue = function(li, issue_url, data) { + return $.ajax({ + type: "PUT", + url: issue_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data, li); + }; + })(this), + error: function(data) { + return new Flash("Issue update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.sortIssues = function(data) { + var sort_issues_url; + sort_issues_url = location.href + "/sort_issues"; + return $.ajax({ + type: "PUT", + url: sort_issues_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data); + }; + })(this), + error: function() { + return new Flash("Issues update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.sortMergeRequests = function(data) { + var sort_mr_url; + sort_mr_url = location.href + "/sort_merge_requests"; + return $.ajax({ + type: "PUT", + url: sort_mr_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data); + }; + })(this), + error: function(data) { + return new Flash("Issue update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.updateMergeRequest = function(li, merge_request_url, data) { + return $.ajax({ + type: "PUT", + url: merge_request_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data, li); + }; + })(this), + error: function(data) { + return new Flash("Issue update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.successCallback = function(data, element) { + var img_tag; + if (data.assignee) { + img_tag = $('<img/>'); + img_tag.attr('src', data.assignee.avatar_url); + img_tag.addClass('avatar s16'); + $(element).find('.assignee-icon').html(img_tag); + } else { + $(element).find('.assignee-icon').html(''); + } + return $(element).effect('highlight'); + }; + + function Milestone() { + var oldMouseStart; + oldMouseStart = $.ui.sortable.prototype._mouseStart; + $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) { + this._trigger("beforeStart", event, this._uiHash()); + return oldMouseStart.apply(this, [event, overrideHandle, noActivation]); + }; + this.bindIssuesSorting(); + this.bindMergeRequestSorting(); + this.bindTabsSwitching(); + } + + Milestone.prototype.bindIssuesSorting = function() { + return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({ + connectWith: ".issues-sortable-list", + dropOnEmpty: true, + items: "li:not(.ui-sort-disabled)", + beforeStart: function(event, ui) { + return $(".issues-sortable-list").css("min-height", ui.item.outerHeight()); + }, + stop: function(event, ui) { + return $(".issues-sortable-list").css("min-height", "0px"); + }, + update: function(event, ui) { + var data; + if ($(this).find(ui.item).length > 0) { + data = $(this).sortable("serialize"); + return Milestone.sortIssues(data); + } + }, + receive: function(event, ui) { + var data, issue_id, issue_url, new_state; + new_state = $(this).data('state'); + issue_id = ui.item.data('iid'); + issue_url = ui.item.data('url'); + data = (function() { + switch (new_state) { + case 'ongoing': + return "issue[assignee_id]=" + gon.current_user_id; + case 'unassigned': + return "issue[assignee_id]="; + case 'closed': + return "issue[state_event]=close"; + } + })(); + if ($(ui.sender).data('state') === "closed") { + data += "&issue[state_event]=reopen"; + } + return Milestone.updateIssue(ui.item, issue_url, data); + } + }).disableSelection(); + }; + + Milestone.prototype.bindTabsSwitching = function() { + return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { + var currentTabClass, previousTabClass; + currentTabClass = $(e.target).data('show'); + previousTabClass = $(e.relatedTarget).data('show'); + $(previousTabClass).hide(); + $(currentTabClass).removeClass('hidden'); + return $(currentTabClass).show(); + }); + }; + + Milestone.prototype.bindMergeRequestSorting = function() { + return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({ + connectWith: ".merge_requests-sortable-list", + dropOnEmpty: true, + items: "li:not(.ui-sort-disabled)", + beforeStart: function(event, ui) { + return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight()); + }, + stop: function(event, ui) { + return $(".merge_requests-sortable-list").css("min-height", "0px"); + }, + update: function(event, ui) { + var data; + data = $(this).sortable("serialize"); + return Milestone.sortMergeRequests(data); + }, + receive: function(event, ui) { + var data, merge_request_id, merge_request_url, new_state; + new_state = $(this).data('state'); + merge_request_id = ui.item.data('iid'); + merge_request_url = ui.item.data('url'); + data = (function() { + switch (new_state) { + case 'ongoing': + return "merge_request[assignee_id]=" + gon.current_user_id; + case 'unassigned': + return "merge_request[assignee_id]="; + case 'closed': + return "merge_request[state_event]=close"; + } + })(); + if ($(ui.sender).data('state') === "closed") { + data += "&merge_request[state_event]=reopen"; + } + return Milestone.updateMergeRequest(ui.item, merge_request_url, data); + } + }).disableSelection(); + }; + + return Milestone; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee deleted file mode 100644 index a19e68b39e2..00000000000 --- a/app/assets/javascripts/milestone.js.coffee +++ /dev/null @@ -1,146 +0,0 @@ -class @Milestone - @updateIssue: (li, issue_url, data) -> - $.ajax - type: "PUT" - url: issue_url - data: data - success: (_data) => - @successCallback(_data, li) - error: (data) -> - new Flash("Issue update failed", 'alert') - dataType: "json" - - @sortIssues: (data) -> - sort_issues_url = location.href + "/sort_issues" - - $.ajax - type: "PUT" - url: sort_issues_url - data: data - success: (_data) => - @successCallback(_data) - error: -> - new Flash("Issues update failed", 'alert') - dataType: "json" - - @sortMergeRequests: (data) -> - sort_mr_url = location.href + "/sort_merge_requests" - - $.ajax - type: "PUT" - url: sort_mr_url - data: data - success: (_data) => - @successCallback(_data) - error: (data) -> - new Flash("Issue update failed", 'alert') - dataType: "json" - - @updateMergeRequest: (li, merge_request_url, data) -> - $.ajax - type: "PUT" - url: merge_request_url - data: data - success: (_data) => - @successCallback(_data, li) - error: (data) -> - new Flash("Issue update failed", 'alert') - dataType: "json" - - @successCallback: (data, element) => - if data.assignee - img_tag = $('<img/>') - img_tag.attr('src', data.assignee.avatar_url) - img_tag.addClass('avatar s16') - $(element).find('.assignee-icon').html(img_tag) - else - $(element).find('.assignee-icon').html('') - - $(element).effect 'highlight' - - constructor: -> - oldMouseStart = $.ui.sortable.prototype._mouseStart - $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) -> - this._trigger "beforeStart", event, this._uiHash() - oldMouseStart.apply this, [event, overrideHandle, noActivation] - - @bindIssuesSorting() - @bindMergeRequestSorting() - @bindTabsSwitching() - - bindIssuesSorting: -> - $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( - connectWith: ".issues-sortable-list", - dropOnEmpty: true, - items: "li:not(.ui-sort-disabled)", - beforeStart: (event, ui) -> - $(".issues-sortable-list").css "min-height", ui.item.outerHeight() - stop: (event, ui) -> - $(".issues-sortable-list").css "min-height", "0px" - update: (event, ui) -> - # Prevents sorting from container which element has been removed. - if $(this).find(ui.item).length > 0 - data = $(this).sortable("serialize") - Milestone.sortIssues(data) - - receive: (event, ui) -> - new_state = $(this).data('state') - issue_id = ui.item.data('iid') - issue_url = ui.item.data('url') - - data = switch new_state - when 'ongoing' - "issue[assignee_id]=" + gon.current_user_id - when 'unassigned' - "issue[assignee_id]=" - when 'closed' - "issue[state_event]=close" - - if $(ui.sender).data('state') == "closed" - data += "&issue[state_event]=reopen" - - Milestone.updateIssue(ui.item, issue_url, data) - - ).disableSelection() - - bindTabsSwitching: -> - $('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> - currentTabClass = $(e.target).data('show') - previousTabClass = $(e.relatedTarget).data('show') - - $(previousTabClass).hide() - $(currentTabClass).removeClass('hidden') - $(currentTabClass).show() - - bindMergeRequestSorting: -> - $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( - connectWith: ".merge_requests-sortable-list", - dropOnEmpty: true, - items: "li:not(.ui-sort-disabled)", - beforeStart: (event, ui) -> - $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight() - stop: (event, ui) -> - $(".merge_requests-sortable-list").css "min-height", "0px" - update: (event, ui) -> - data = $(this).sortable("serialize") - Milestone.sortMergeRequests(data) - - receive: (event, ui) -> - new_state = $(this).data('state') - merge_request_id = ui.item.data('iid') - merge_request_url = ui.item.data('url') - - data = switch new_state - when 'ongoing' - "merge_request[assignee_id]=" + gon.current_user_id - when 'unassigned' - "merge_request[assignee_id]=" - when 'closed' - "merge_request[state_event]=close" - - if $(ui.sender).data('state') == "closed" - data += "&merge_request[state_event]=reopen" - - Milestone.updateMergeRequest(ui.item, merge_request_url, data) - - ).disableSelection() diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js new file mode 100644 index 00000000000..a0b65d20c03 --- /dev/null +++ b/app/assets/javascripts/milestone_select.js @@ -0,0 +1,151 @@ +(function() { + this.MilestoneSelect = (function() { + function MilestoneSelect(currentProject) { + var _this; + if (currentProject != null) { + _this = this; + this.currentProject = JSON.parse(currentProject); + } + $('.js-milestone-select').each(function(i, dropdown) { + var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId; + $dropdown = $(dropdown); + projectId = $dropdown.data('project-id'); + milestonesUrl = $dropdown.data('milestones'); + issueUpdateURL = $dropdown.data('issueUpdate'); + selectedMilestone = $dropdown.data('selected'); + showNo = $dropdown.data('show-no'); + showAny = $dropdown.data('show-any'); + showUpcoming = $dropdown.data('show-upcoming'); + useId = $dropdown.data('use-id'); + defaultLabel = $dropdown.data('default-label'); + issuableId = $dropdown.data('issuable-id'); + abilityName = $dropdown.data('ability-name'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); + $value = $block.find('.value'); + $loading = $block.find('.block-loading').fadeOut(); + if (issueUpdateURL) { + milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); + milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; + collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); + } + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: milestonesUrl + }).done(function(data) { + var extraOptions; + extraOptions = []; + if (showAny) { + extraOptions.push({ + id: 0, + name: '', + title: 'Any Milestone' + }); + } + if (showNo) { + extraOptions.push({ + id: -1, + name: 'No Milestone', + title: 'No Milestone' + }); + } + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: 'Upcoming' + }); + } + if (extraOptions.length > 2) { + extraOptions.push('divider'); + } + return callback(extraOptions.concat(data)); + }); + }, + filterable: true, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel: function(selected) { + if (selected && 'id' in selected) { + return selected.title; + } else { + return defaultLabel; + } + }, + fieldName: $dropdown.data('field-name'), + text: function(milestone) { + return _.escape(milestone.title); + }, + id: function(milestone) { + if (!useId) { + return milestone.name; + } else { + return milestone.id; + } + }, + isSelected: function(milestone) { + return milestone.name === selectedMilestone; + }, + hidden: function() { + $selectbox.hide(); + return $value.css('display', ''); + }, + clicked: function(selected) { + var data, isIssueIndex, isMRIndex, page; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = (page === page && page === 'projects:merge_requests:index'); + if ($dropdown.hasClass('js-filter-bulk-update')) { + return; + } + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + if (selected.name != null) { + selectedMilestone = selected.name; + } else { + selectedMilestone = ''; + } + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else { + selected = $selectbox.find('input[type="hidden"]').val(); + data = {}; + data[abilityName] = {}; + data[abilityName].milestone_id = selected != null ? selected : null; + $loading.fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + data: data + }).done(function(data) { + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + $selectbox.hide(); + $value.css('display', ''); + if (data.milestone != null) { + data.milestone.namespace = _this.currentProject.namespace; + data.milestone.path = _this.currentProject.path; + data.milestone.remaining = $.timefor(data.milestone.due_date); + $value.html(milestoneLinkTemplate(data.milestone)); + return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); + } else { + $value.html(milestoneLinkNoneTemplate); + return $sidebarCollapsedValue.find('span').text('No'); + } + }); + } + } + }); + }); + } + + return MilestoneSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee deleted file mode 100644 index 8ab03ed93ee..00000000000 --- a/app/assets/javascripts/milestone_select.js.coffee +++ /dev/null @@ -1,137 +0,0 @@ -class @MilestoneSelect - constructor: (currentProject) -> - if currentProject? - _this = @ - @currentProject = JSON.parse(currentProject) - $('.js-milestone-select').each (i, dropdown) -> - $dropdown = $(dropdown) - projectId = $dropdown.data('project-id') - milestonesUrl = $dropdown.data('milestones') - issueUpdateURL = $dropdown.data('issueUpdate') - selectedMilestone = $dropdown.data('selected') - showNo = $dropdown.data('show-no') - showAny = $dropdown.data('show-any') - showUpcoming = $dropdown.data('show-upcoming') - useId = $dropdown.data('use-id') - defaultLabel = $dropdown.data('default-label') - issuableId = $dropdown.data('issuable-id') - abilityName = $dropdown.data('ability-name') - $selectbox = $dropdown.closest('.selectbox') - $block = $selectbox.closest('.block') - $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon') - $value = $block.find('.value') - $loading = $block.find('.block-loading').fadeOut() - - if issueUpdateURL - milestoneLinkTemplate = _.template( - '<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>' - ) - - milestoneLinkNoneTemplate = '<span class="no-value">None</span>' - - collapsedSidebarLabelTemplate = _.template( - '<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> - <%- title %> - </span>' - ) - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: milestonesUrl - ).done (data) -> - extraOptions = [] - if showAny - extraOptions.push( - id: 0 - name: '' - title: 'Any Milestone' - ) - - if showNo - extraOptions.push( - id: -1 - name: 'No Milestone' - title: 'No Milestone' - ) - - if showUpcoming - extraOptions.push( - id: -2 - name: '#upcoming' - title: 'Upcoming' - ) - - if extraOptions.length > 2 - extraOptions.push 'divider' - - callback(extraOptions.concat(data)) - filterable: true - search: - fields: ['title'] - selectable: true - toggleLabel: (selected) -> - if selected && 'id' of selected - selected.title - else - defaultLabel - fieldName: $dropdown.data('field-name') - text: (milestone) -> - _.escape(milestone.title) - id: (milestone) -> - if !useId - milestone.name - else - milestone.id - isSelected: (milestone) -> - milestone.name is selectedMilestone - hidden: -> - $selectbox.hide() - - # display:block overrides the hide-collapse rule - $value.css('display', '') - clicked: (selected) -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - - if $dropdown.hasClass 'js-filter-bulk-update' - return - - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - if selected.name? - selectedMilestone = selected.name - else - selectedMilestone = '' - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass('js-filter-submit') - $dropdown.closest('form').submit() - else - selected = $selectbox - .find('input[type="hidden"]') - .val() - data = {} - data[abilityName] = {} - data[abilityName].milestone_id = if selected? then selected else null - $loading - .fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $.ajax( - type: 'PUT' - url: issueUpdateURL - data: data - ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $loading.fadeOut() - $selectbox.hide() - $value.css('display', '') - if data.milestone? - data.milestone.namespace = _this.currentProject.namespace - data.milestone.path = _this.currentProject.path - data.milestone.remaining = $.timefor data.milestone.due_date - $value.html(milestoneLinkTemplate(data.milestone)) - $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)) - else - $value.html(milestoneLinkNoneTemplate) - $sidebarCollapsedValue.find('span').text('No') - ) diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js new file mode 100644 index 00000000000..10f4fd106d8 --- /dev/null +++ b/app/assets/javascripts/namespace_select.js @@ -0,0 +1,86 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.NamespaceSelect = (function() { + function NamespaceSelect(opts) { + this.onSelectItem = bind(this.onSelectItem, this); + var fieldName, showAny; + this.dropdown = opts.dropdown; + showAny = true; + fieldName = 'namespace_id'; + if (this.dropdown.attr('data-field-name')) { + fieldName = this.dropdown.data('fieldName'); + } + if (this.dropdown.attr('data-show-any')) { + showAny = this.dropdown.data('showAny'); + } + this.dropdown.glDropdown({ + filterable: true, + selectable: true, + filterRemote: true, + search: { + fields: ['path'] + }, + fieldName: fieldName, + toggleLabel: function(selected) { + if (selected.id == null) { + return selected.text; + } else { + return selected.kind + ": " + selected.path; + } + }, + data: function(term, dataCallback) { + return Api.namespaces(term, function(namespaces) { + var anyNamespace; + if (showAny) { + anyNamespace = { + text: 'Any namespace', + id: null + }; + namespaces.unshift(anyNamespace); + namespaces.splice(1, 0, 'divider'); + } + return dataCallback(namespaces); + }); + }, + text: function(namespace) { + if (namespace.id == null) { + return namespace.text; + } else { + return namespace.kind + ": " + namespace.path; + } + }, + renderRow: this.renderRow, + clicked: this.onSelectItem + }); + } + + NamespaceSelect.prototype.onSelectItem = function(item, el, e) { + return e.preventDefault(); + }; + + return NamespaceSelect; + + })(); + + this.NamespaceSelects = (function() { + function NamespaceSelects(opts) { + var ref; + if (opts == null) { + opts = {}; + } + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select'); + this.$dropdowns.each(function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new NamespaceSelect({ + dropdown: $dropdown + }); + }); + } + + return NamespaceSelects; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee deleted file mode 100644 index 3b419dff105..00000000000 --- a/app/assets/javascripts/namespace_select.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -class @NamespaceSelect - constructor: (opts) -> - { - @dropdown - } = opts - - showAny = true - fieldName = 'namespace_id' - - if @dropdown.attr 'data-field-name' - fieldName = @dropdown.data 'fieldName' - - if @dropdown.attr 'data-show-any' - showAny = @dropdown.data 'showAny' - - @dropdown.glDropdown( - filterable: true - selectable: true - filterRemote: true - search: - fields: ['path'] - fieldName: fieldName - toggleLabel: (selected) -> - return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}" - data: (term, dataCallback) -> - Api.namespaces term, (namespaces) -> - if showAny - anyNamespace = - text: 'Any namespace' - id: null - - namespaces.unshift(anyNamespace) - namespaces.splice 1, 0, 'divider' - - dataCallback(namespaces) - text: (namespace) -> - return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}" - renderRow: @renderRow - clicked: @onSelectItem - ) - - onSelectItem: (item, el, e) => - e.preventDefault() - -class @NamespaceSelects - constructor: (opts = {}) -> - { - @$dropdowns = $('.js-namespace-select') - } = opts - - @$dropdowns.each (i, dropdown) -> - $dropdown = $(dropdown) - - new NamespaceSelect( - dropdown: $dropdown - ) diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js new file mode 100644 index 00000000000..c0fec1f8607 --- /dev/null +++ b/app/assets/javascripts/network/branch-graph.js @@ -0,0 +1,404 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.BranchGraph = (function() { + function BranchGraph(element1, options1) { + this.element = element1; + this.options = options1; + this.scrollTop = bind(this.scrollTop, this); + this.scrollBottom = bind(this.scrollBottom, this); + this.scrollRight = bind(this.scrollRight, this); + this.scrollLeft = bind(this.scrollLeft, this); + this.scrollUp = bind(this.scrollUp, this); + this.scrollDown = bind(this.scrollDown, this); + this.preparedCommits = {}; + this.mtime = 0; + this.mspace = 0; + this.parents = {}; + this.colors = ["#000"]; + this.offsetX = 150; + this.offsetY = 20; + this.unitTime = 30; + this.unitSpace = 10; + this.prev_start = -1; + this.load(); + } + + BranchGraph.prototype.load = function() { + return $.ajax({ + url: this.options.url, + method: "get", + dataType: "json", + success: $.proxy(function(data) { + $(".loading", this.element).hide(); + this.prepareData(data.days, data.commits); + return this.buildGraph(); + }, this) + }); + }; + + BranchGraph.prototype.prepareData = function(days, commits) { + var c, ch, cw, j, len, ref; + this.days = days; + this.commits = commits; + this.collectParents(); + this.graphHeight = $(this.element).height(); + this.graphWidth = $(this.element).width(); + ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); + cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); + this.r = Raphael(this.element.get(0), cw, ch); + this.top = this.r.set(); + this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); + ref = this.commits; + for (j = 0, len = ref.length; j < len; j++) { + c = ref[j]; + if (c.id in this.parents) { + c.isParent = true; + } + this.preparedCommits[c.id] = c; + this.markCommit(c); + } + return this.collectColors(); + }; + + BranchGraph.prototype.collectParents = function() { + var c, j, len, p, ref, results; + ref = this.commits; + results = []; + for (j = 0, len = ref.length; j < len; j++) { + c = ref[j]; + this.mtime = Math.max(this.mtime, c.time); + this.mspace = Math.max(this.mspace, c.space); + results.push((function() { + var l, len1, ref1, results1; + ref1 = c.parents; + results1 = []; + for (l = 0, len1 = ref1.length; l < len1; l++) { + p = ref1[l]; + this.parents[p[0]] = true; + results1.push(this.mspace = Math.max(this.mspace, p[1])); + } + return results1; + }).call(this)); + } + return results; + }; + + BranchGraph.prototype.collectColors = function() { + var k, results; + k = 0; + results = []; + while (k < this.mspace) { + this.colors.push(Raphael.getColor(.8)); + Raphael.getColor(); + Raphael.getColor(); + results.push(k++); + } + return results; + }; + + BranchGraph.prototype.buildGraph = function() { + var cuday, cumonth, day, j, len, mm, r, ref; + r = this.r; + cuday = 0; + cumonth = ""; + r.rect(0, 0, 40, this.barHeight).attr({ + fill: "#222" + }); + r.rect(40, 0, 30, this.barHeight).attr({ + fill: "#444" + }); + ref = this.days; + for (mm = j = 0, len = ref.length; j < len; mm = ++j) { + day = ref[mm]; + if (cuday !== day[0] || cumonth !== day[1]) { + r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ + font: "12px Monaco, monospace", + fill: "#BBB" + }); + cuday = day[0]; + } + if (cumonth !== day[1]) { + r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ + font: "12px Monaco, monospace", + fill: "#EEE" + }); + cumonth = day[1]; + } + } + this.renderPartialGraph(); + return this.bindEvents(); + }; + + BranchGraph.prototype.renderPartialGraph = function() { + var commit, end, i, isGraphEdge, start, x, y; + start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10; + if (start < 0) { + isGraphEdge = true; + start = 0; + } + end = start + 40; + if (this.commits.length < end) { + isGraphEdge = true; + end = this.commits.length; + } + if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) { + i = start; + this.prev_start = start; + while (i < end) { + commit = this.commits[i]; + i += 1; + if (commit.hasDrawn !== true) { + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + this.drawDot(x, y, commit); + this.drawLines(x, y, commit); + this.appendLabel(x, y, commit); + this.appendAnchor(x, y, commit); + commit.hasDrawn = true; + } + } + return this.top.toFront(); + } + }; + + BranchGraph.prototype.bindEvents = function() { + var element; + element = this.element; + return $(element).scroll((function(_this) { + return function(event) { + return _this.renderPartialGraph(); + }; + })(this)); + }; + + BranchGraph.prototype.scrollDown = function() { + this.element.scrollTop(this.element.scrollTop() + 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollUp = function() { + this.element.scrollTop(this.element.scrollTop() - 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollLeft = function() { + this.element.scrollLeft(this.element.scrollLeft() - 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollRight = function() { + this.element.scrollLeft(this.element.scrollLeft() + 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollBottom = function() { + return this.element.scrollTop(this.element.find('svg').height()); + }; + + BranchGraph.prototype.scrollTop = function() { + return this.element.scrollTop(0); + }; + + BranchGraph.prototype.appendLabel = function(x, y, commit) { + var label, r, rect, shortrefs, text, textbox, triangle; + if (!commit.refs) { + return; + } + r = this.r; + shortrefs = commit.refs; + if (shortrefs.length > 17) { + shortrefs = shortrefs.substr(0, 15) + "…"; + } + text = r.text(x + 4, y, shortrefs).attr({ + "text-anchor": "start", + font: "10px Monaco, monospace", + fill: "#FFF", + title: commit.refs + }); + textbox = text.getBBox(); + rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + label = r.set(rect, text); + label.transform(["t", -rect.getBBox().width - 15, 0]); + return text.toFront(); + }; + + BranchGraph.prototype.appendAnchor = function(x, y, commit) { + var anchor, options, r, top; + r = this.r; + top = this.top; + options = this.options; + anchor = r.circle(x, y, 10).attr({ + fill: "#000", + opacity: 0, + cursor: "pointer" + }).click(function() { + return window.open(options.commit_url.replace("%s", commit.id), "_blank"); + }).hover(function() { + this.tooltip = r.commitTooltip(x + 5, y, commit); + return top.push(this.tooltip.insertBefore(this)); + }, function() { + return this.tooltip && this.tooltip.remove() && delete this.tooltip; + }); + return top.push(anchor); + }; + + BranchGraph.prototype.drawDot = function(x, y, commit) { + var avatar_box_x, avatar_box_y, r; + r = this.r; + r.circle(x, y, 3).attr({ + fill: this.colors[commit.space], + stroke: "none" + }); + avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; + avatar_box_y = y - 10; + r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ + stroke: this.colors[commit.space], + "stroke-width": 2 + }); + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); + return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ + "text-anchor": "start", + font: "14px Monaco, monospace" + }); + }; + + BranchGraph.prototype.drawLines = function(x, y, commit) { + var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; + r = this.r; + ref = commit.parents; + results = []; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + parent = ref[i]; + parentCommit = this.preparedCommits[parent[0]]; + 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]); + if (parentCommit.space <= commit.space) { + color = this.colors[commit.space]; + } else { + color = this.colors[parentCommit.space]; + } + if (parent[1] === commit.space) { + offset = [0, 5]; + arrow = "l-2,5,4,0,-2,-5,0,5"; + } else if (parent[1] < commit.space) { + offset = [3, 3]; + arrow = "l5,0,-2,4,-3,-4,4,2"; + } else { + offset = [-3, 3]; + arrow = "l-5,0,2,4,3,-4,-4,2"; + } + route = ["M", x + offset[0], y + offset[1]]; + if (i > 0) { + route.push(arrow); + } + if (commit.space !== parentCommit.space || commit.space !== parent[1]) { + route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); + } + route.push("L", parentX1, parentY); + results.push(r.path(route).attr({ + stroke: color, + "stroke-width": 2 + })); + } + return results; + }; + + BranchGraph.prototype.markCommit = function(commit) { + var r, x, y; + if (commit.id === this.options.commit_id) { + r = this.r; + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + return this.element.scrollTop(y - this.graphHeight / 2); + } + }; + + return BranchGraph; + + })(); + + Raphael.prototype.commitTooltip = function(x, y, commit) { + var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip; + boxWidth = 300; + boxHeight = 200; + icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); + nameText = this.text(x + 25, y + 10, commit.author.name); + idText = this.text(x, y + 35, commit.id); + messageText = this.text(x, y + 50, commit.message); + textSet = this.set(icon, nameText, idText, messageText).attr({ + "text-anchor": "start", + font: "12px Monaco, monospace" + }); + nameText.attr({ + font: "14px Arial", + "font-weight": "bold" + }); + idText.attr({ + fill: "#AAA" + }); + this.textWrap(messageText, boxWidth - 50); + rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ + fill: "#FFF", + stroke: "#000", + "stroke-linecap": "round", + "stroke-width": 2 + }); + tooltip = this.set(rect, textSet); + rect.attr({ + height: tooltip.getBBox().height + 10, + width: tooltip.getBBox().width + 10 + }); + tooltip.transform(["t", 20, 20]); + return tooltip; + }; + + Raphael.prototype.textWrap = function(t, width) { + var abc, b, content, h, j, len, letterWidth, s, word, words, x; + content = t.attr("text"); + abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + t.attr({ + text: abc + }); + letterWidth = t.getBBox().width / abc.length; + t.attr({ + text: content + }); + words = content.split(" "); + x = 0; + s = []; + for (j = 0, len = words.length; j < len; j++) { + word = words[j]; + if (x + (word.length * letterWidth) > width) { + s.push("\n"); + x = 0; + } + x += word.length * letterWidth; + s.push(word + " "); + } + t.attr({ + text: s.join("") + }); + b = t.getBBox(); + h = Math.abs(b.y2) - Math.abs(b.y) + 1; + return t.attr({ + y: b.y + h + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/network/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee deleted file mode 100644 index f2fd2a775a4..00000000000 --- a/app/assets/javascripts/network/branch-graph.js.coffee +++ /dev/null @@ -1,340 +0,0 @@ -class @BranchGraph - constructor: (@element, @options) -> - @preparedCommits = {} - @mtime = 0 - @mspace = 0 - @parents = {} - @colors = ["#000"] - @offsetX = 150 - @offsetY = 20 - @unitTime = 30 - @unitSpace = 10 - @prev_start = -1 - @load() - - load: -> - $.ajax - url: @options.url - method: "get" - dataType: "json" - success: $.proxy((data) -> - $(".loading", @element).hide() - @prepareData data.days, data.commits - @buildGraph() - , this) - - prepareData: (@days, @commits) -> - @collectParents() - @graphHeight = $(@element).height() - @graphWidth = $(@element).width() - ch = Math.max(@graphHeight, @offsetY + @unitTime * @mtime + 150) - cw = Math.max(@graphWidth, @offsetX + @unitSpace * @mspace + 300) - @r = Raphael(@element.get(0), cw, ch) - @top = @r.set() - @barHeight = Math.max(@graphHeight, @unitTime * @days.length + 320) - - for c in @commits - c.isParent = true if c.id of @parents - @preparedCommits[c.id] = c - @markCommit(c) - - @collectColors() - - collectParents: -> - for c in @commits - @mtime = Math.max(@mtime, c.time) - @mspace = Math.max(@mspace, c.space) - for p in c.parents - @parents[p[0]] = true - @mspace = Math.max(@mspace, p[1]) - - collectColors: -> - k = 0 - while k < @mspace - @colors.push Raphael.getColor(.8) - # Skipping a few colors in the spectrum to get more contrast between colors - Raphael.getColor() - Raphael.getColor() - k++ - - buildGraph: -> - r = @r - cuday = 0 - cumonth = "" - - r.rect(0, 0, 40, @barHeight).attr fill: "#222" - r.rect(40, 0, 30, @barHeight).attr fill: "#444" - - for day, mm in @days - if cuday isnt day[0] || cumonth isnt day[1] - # Dates - r.text(55, @offsetY + @unitTime * mm, day[0]) - .attr( - font: "12px Monaco, monospace" - fill: "#BBB" - ) - cuday = day[0] - - if cumonth isnt day[1] - # Months - r.text(20, @offsetY + @unitTime * mm, day[1]) - .attr( - font: "12px Monaco, monospace" - fill: "#EEE" - ) - cumonth = day[1] - - @renderPartialGraph() - - @bindEvents() - - renderPartialGraph: -> - start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10 - if start < 0 - isGraphEdge = true - start = 0 - end = start + 40 - if @commits.length < end - isGraphEdge = true - end = @commits.length - - if @prev_start == -1 or Math.abs(@prev_start - start) > 10 or isGraphEdge - i = start - - @prev_start = start - - while i < end - commit = @commits[i] - i += 1 - - if commit.hasDrawn isnt true - x = @offsetX + @unitSpace * (@mspace - commit.space) - y = @offsetY + @unitTime * commit.time - - @drawDot(x, y, commit) - - @drawLines(x, y, commit) - - @appendLabel(x, y, commit) - - @appendAnchor(x, y, commit) - - commit.hasDrawn = true - - @top.toFront() - - bindEvents: -> - element = @element - - $(element).scroll (event) => - @renderPartialGraph() - - scrollDown: => - @element.scrollTop @element.scrollTop() + 50 - @renderPartialGraph() - - scrollUp: => - @element.scrollTop @element.scrollTop() - 50 - @renderPartialGraph() - - scrollLeft: => - @element.scrollLeft @element.scrollLeft() - 50 - @renderPartialGraph() - - scrollRight: => - @element.scrollLeft @element.scrollLeft() + 50 - @renderPartialGraph() - - scrollBottom: => - @element.scrollTop @element.find('svg').height() - - scrollTop: => - @element.scrollTop 0 - - appendLabel: (x, y, commit) -> - return unless commit.refs - - r = @r - shortrefs = commit.refs - # Truncate if longer than 15 chars - shortrefs = shortrefs.substr(0, 15) + "…" if shortrefs.length > 17 - text = r.text(x + 4, y, shortrefs).attr( - "text-anchor": "start" - font: "10px Monaco, monospace" - fill: "#FFF" - 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 - stroke: "none" - ) - triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr( - fill: "#000" - "fill-opacity": .5 - stroke: "none" - ) - - label = r.set(rect, text) - label.transform(["t", -rect.getBBox().width - 15, 0]) - - # Set text to front - text.toFront() - - appendAnchor: (x, y, commit) -> - r = @r - top = @top - options = @options - anchor = r.circle(x, y, 10).attr( - fill: "#000" - opacity: 0 - cursor: "pointer" - ).click(-> - window.open options.commit_url.replace("%s", commit.id), "_blank" - ).hover(-> - @tooltip = r.commitTooltip(x + 5, y, commit) - top.push @tooltip.insertBefore(this) - , -> - @tooltip and @tooltip.remove() and delete @tooltip - ) - top.push anchor - - drawDot: (x, y, commit) -> - r = @r - r.circle(x, y, 3).attr( - fill: @colors[commit.space] - stroke: "none" - ) - - avatar_box_x = @offsetX + @unitSpace * @mspace + 10 - avatar_box_y = y - 10 - r.rect(avatar_box_x, avatar_box_y, 20, 20).attr( - stroke: @colors[commit.space] - "stroke-width": 2 - ) - r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) - r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr( - "text-anchor": "start" - font: "14px Monaco, monospace" - ) - - drawLines: (x, y, commit) -> - r = @r - for parent, i in commit.parents - parentCommit = @preparedCommits[parent[0]] - parentY = @offsetY + @unitTime * parentCommit.time - parentX1 = @offsetX + @unitSpace * (@mspace - parentCommit.space) - parentX2 = @offsetX + @unitSpace * (@mspace - parent[1]) - - # Set line color - if parentCommit.space <= commit.space - color = @colors[commit.space] - - else - color = @colors[parentCommit.space] - - # Build line shape - if parent[1] is commit.space - offset = [0, 5] - arrow = "l-2,5,4,0,-2,-5,0,5" - - else if parent[1] < commit.space - offset = [3, 3] - arrow = "l5,0,-2,4,-3,-4,4,2" - - else - 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 isnt parentCommit.space or commit.space isnt parent[1] - route.push( - "L", parentX2, y + 10, - "L", parentX2, parentY - 5, - ) - - # End point - route.push("L", parentX1, parentY) - - r - .path(route) - .attr( - stroke: color - "stroke-width": 2) - - markCommit: (commit) -> - if commit.id is @options.commit_id - r = @r - x = @offsetX + @unitSpace * (@mspace - commit.space) - y = @offsetY + @unitTime * commit.time - r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr( - fill: "#000" - "fill-opacity": .5 - stroke: "none" - ) - # Displayed in the center - @element.scrollTop(y - @graphHeight / 2) - -Raphael::commitTooltip = (x, y, commit) -> - boxWidth = 300 - boxHeight = 200 - icon = @image(gon.relative_url_root + commit.author.icon, x, y, 20, 20) - nameText = @text(x + 25, y + 10, commit.author.name) - idText = @text(x, y + 35, commit.id) - messageText = @text(x, y + 50, commit.message) - textSet = @set(icon, nameText, idText, messageText).attr( - "text-anchor": "start" - font: "12px Monaco, monospace" - ) - nameText.attr( - font: "14px Arial" - "font-weight": "bold" - ) - - idText.attr fill: "#AAA" - @textWrap messageText, boxWidth - 50 - rect = @rect(x - 10, y - 10, boxWidth, 100, 4).attr( - fill: "#FFF" - stroke: "#000" - "stroke-linecap": "round" - "stroke-width": 2 - ) - tooltip = @set(rect, textSet) - rect.attr( - height: tooltip.getBBox().height + 10 - width: tooltip.getBBox().width + 10 - ) - - tooltip.transform ["t", 20, 20] - tooltip - -Raphael::textWrap = (t, width) -> - content = t.attr("text") - abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - t.attr text: abc - letterWidth = t.getBBox().width / abc.length - t.attr text: content - words = content.split(" ") - x = 0 - s = [] - - for word in words - if x + (word.length * letterWidth) > width - s.push "\n" - x = 0 - x += word.length * letterWidth - s.push word + " " - - t.attr text: s.join("") - b = t.getBBox() - h = Math.abs(b.y2) - Math.abs(b.y) + 1 - t.attr y: b.y + h diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js new file mode 100644 index 00000000000..7baebcd100a --- /dev/null +++ b/app/assets/javascripts/network/network.js @@ -0,0 +1,19 @@ +(function() { + this.Network = (function() { + function Network(opts) { + var vph; + $("#filter_ref").click(function() { + return $(this).closest('form').submit(); + }); + this.branch_graph = new BranchGraph($(".network-graph"), opts); + vph = $(window).height() - 250; + $('.network-graph').css({ + 'height': vph + 'px' + }); + } + + return Network; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/network/network.js.coffee b/app/assets/javascripts/network/network.js.coffee deleted file mode 100644 index f4ef07a50a7..00000000000 --- a/app/assets/javascripts/network/network.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -class @Network - constructor: (opts) -> - $("#filter_ref").click -> - $(this).closest('form').submit() - - @branch_graph = new BranchGraph($(".network-graph"), opts) - - vph = $(window).height() - 250 - $('.network-graph').css 'height': (vph + 'px') diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js new file mode 100644 index 00000000000..6a7422a7755 --- /dev/null +++ b/app/assets/javascripts/network/network_bundle.js @@ -0,0 +1,16 @@ + +/*= require_tree . */ + +(function() { + $(function() { + var network_graph; + network_graph = new Network({ + url: $(".network-graph").attr('data-url'), + commit_url: $(".network-graph").attr('data-commit-url'), + ref: $(".network-graph").attr('data-ref'), + commit_id: $(".network-graph").attr('data-commit-id') + }); + return new ShortcutsNetwork(network_graph.branch_graph); + }); + +}).call(this); diff --git a/app/assets/javascripts/network/network_bundle.js.coffee b/app/assets/javascripts/network/network_bundle.js.coffee deleted file mode 100644 index f75f63869c5..00000000000 --- a/app/assets/javascripts/network/network_bundle.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -# 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 . - -$ -> - network_graph = new Network({ - url: $(".network-graph").attr('data-url'), - commit_url: $(".network-graph").attr('data-commit-url'), - ref: $(".network-graph").attr('data-ref'), - commit_id: $(".network-graph").attr('data-commit-id') - }) - - new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js new file mode 100644 index 00000000000..20aa2fced27 --- /dev/null +++ b/app/assets/javascripts/new_branch_form.js @@ -0,0 +1,104 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + this.NewBranchForm = (function() { + function NewBranchForm(form, availableRefs) { + this.validate = bind(this.validate, this); + this.branchNameError = form.find('.js-branch-name-error'); + this.name = form.find('.js-branch-name'); + this.ref = form.find('#ref'); + this.setupAvailableRefs(availableRefs); + this.setupRestrictions(); + this.addBinding(); + this.init(); + } + + NewBranchForm.prototype.addBinding = function() { + return this.name.on('blur', this.validate); + }; + + NewBranchForm.prototype.init = function() { + if (this.name.val().length > 0) { + return this.name.trigger('blur'); + } + }; + + NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { + return this.ref.autocomplete({ + source: availableRefs, + minLength: 1 + }); + }; + + NewBranchForm.prototype.setupRestrictions = function() { + var endsWith, invalid, single, startsWith; + startsWith = { + pattern: /^(\/|\.)/g, + prefix: "can't start with", + conjunction: "or" + }; + endsWith = { + pattern: /(\/|\.|\.lock)$/g, + prefix: "can't end in", + conjunction: "or" + }; + invalid = { + pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, + prefix: "can't contain", + conjunction: ", " + }; + single = { + pattern: /^@+$/g, + prefix: "can't be", + conjunction: "or" + }; + return this.restrictions = [startsWith, invalid, endsWith, single]; + }; + + NewBranchForm.prototype.validate = function() { + var errorMessage, errors, formatter, unique, validator; + this.branchNameError.empty(); + unique = function(values, value) { + if (indexOf.call(values, value) < 0) { + values.push(value); + } + return values; + }; + formatter = function(values, restriction) { + var formatted; + formatted = values.map(function(value) { + switch (false) { + case !/\s/.test(value): + return 'spaces'; + case !/\/{2,}/g.test(value): + return 'consecutive slashes'; + default: + return "'" + value + "'"; + } + }); + return restriction.prefix + " " + (formatted.join(restriction.conjunction)); + }; + validator = (function(_this) { + return function(errors, restriction) { + var matched; + matched = _this.name.val().match(restriction.pattern); + if (matched) { + return errors.concat(formatter(matched.reduce(unique, []), restriction)); + } else { + return errors; + } + }; + })(this); + errors = this.restrictions.reduce(validator, []); + if (errors.length > 0) { + errorMessage = $("<span/>").text(errors.join(', ')); + return this.branchNameError.append(errorMessage); + } + }; + + return NewBranchForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/new_branch_form.js.coffee b/app/assets/javascripts/new_branch_form.js.coffee deleted file mode 100644 index 4b350854f78..00000000000 --- a/app/assets/javascripts/new_branch_form.js.coffee +++ /dev/null @@ -1,78 +0,0 @@ -class @NewBranchForm - constructor: (form, availableRefs) -> - @branchNameError = form.find('.js-branch-name-error') - @name = form.find('.js-branch-name') - @ref = form.find('#ref') - - @setupAvailableRefs(availableRefs) - @setupRestrictions() - @addBinding() - @init() - - addBinding: -> - @name.on 'blur', @validate - - init: -> - @name.trigger 'blur' if @name.val().length > 0 - - setupAvailableRefs: (availableRefs) -> - @ref.autocomplete - source: availableRefs, - minLength: 1 - - setupRestrictions: -> - startsWith = { - pattern: /^(\/|\.)/g, - prefix: "can't start with", - conjunction: "or" - } - - endsWith = { - pattern: /(\/|\.|\.lock)$/g, - prefix: "can't end in", - conjunction: "or" - } - - invalid = { - pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g - prefix: "can't contain", - conjunction: ", " - } - - single = { - pattern: /^@+$/g - prefix: "can't be", - conjunction: "or" - } - - @restrictions = [startsWith, invalid, endsWith, single] - - validate: => - @branchNameError.empty() - - unique = (values, value) -> - values.push(value) unless value in values - values - - formatter = (values, restriction) -> - formatted = values.map (value) -> - switch - when /\s/.test value then 'spaces' - when /\/{2,}/g.test value then 'consecutive slashes' - else "'#{value}'" - - "#{restriction.prefix} #{formatted.join(restriction.conjunction)}" - - validator = (errors, restriction) => - matched = @name.val().match(restriction.pattern) - - if matched - errors.concat formatter(matched.reduce(unique, []), restriction) - else - errors - - errors = @restrictions.reduce validator, [] - - if errors.length > 0 - errorMessage = $("<span/>").text(errors.join(', ')) - @branchNameError.append(errorMessage) diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js new file mode 100644 index 00000000000..21bf8867f7b --- /dev/null +++ b/app/assets/javascripts/new_commit_form.js @@ -0,0 +1,34 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.NewCommitForm = (function() { + function NewCommitForm(form) { + this.renderDestination = bind(this.renderDestination, this); + this.newBranch = form.find('.js-target-branch'); + this.originalBranch = form.find('.js-original-branch'); + this.createMergeRequest = form.find('.js-create-merge-request'); + this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); + this.renderDestination(); + this.newBranch.keyup(this.renderDestination); + } + + NewCommitForm.prototype.renderDestination = function() { + var different; + different = this.newBranch.val() !== this.originalBranch.val(); + if (different) { + this.createMergeRequestContainer.show(); + if (!this.wasDifferent) { + this.createMergeRequest.prop('checked', true); + } + } else { + this.createMergeRequestContainer.hide(); + this.createMergeRequest.prop('checked', false); + } + return this.wasDifferent = different; + }; + + return NewCommitForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee deleted file mode 100644 index 03f0f51acfa..00000000000 --- a/app/assets/javascripts/new_commit_form.js.coffee +++ /dev/null @@ -1,21 +0,0 @@ -class @NewCommitForm - constructor: (form) -> - @newBranch = form.find('.js-target-branch') - @originalBranch = form.find('.js-original-branch') - @createMergeRequest = form.find('.js-create-merge-request') - @createMergeRequestContainer = form.find('.js-create-merge-request-container') - - @renderDestination() - @newBranch.keyup @renderDestination - - renderDestination: => - different = @newBranch.val() != @originalBranch.val() - - if different - @createMergeRequestContainer.show() - @createMergeRequest.prop('checked', true) unless @wasDifferent - else - @createMergeRequestContainer.hide() - @createMergeRequest.prop('checked', false) - - @wasDifferent = different diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js new file mode 100644 index 00000000000..9ece474d994 --- /dev/null +++ b/app/assets/javascripts/notes.js @@ -0,0 +1,732 @@ + +/*= require autosave */ + + +/*= require autosize */ + + +/*= require dropzone */ + + +/*= require dropzone_input */ + + +/*= require gfm_auto_complete */ + + +/*= require jquery.atwho */ + + +/*= require task_list */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Notes = (function() { + var isMetaKey; + + Notes.interval = null; + + function Notes(notes_url, note_ids, last_fetched_at, view) { + this.updateTargetButtons = bind(this.updateTargetButtons, this); + this.updateCloseButton = bind(this.updateCloseButton, this); + this.visibilityChange = bind(this.visibilityChange, this); + this.cancelDiscussionForm = bind(this.cancelDiscussionForm, this); + this.addDiffNote = bind(this.addDiffNote, this); + this.setupDiscussionNoteForm = bind(this.setupDiscussionNoteForm, this); + this.replyToDiscussionNote = bind(this.replyToDiscussionNote, this); + this.removeNote = bind(this.removeNote, this); + this.cancelEdit = bind(this.cancelEdit, this); + this.updateNote = bind(this.updateNote, this); + this.addDiscussionNote = bind(this.addDiscussionNote, this); + this.addNoteError = bind(this.addNoteError, this); + this.addNote = bind(this.addNote, this); + this.resetMainTargetForm = bind(this.resetMainTargetForm, this); + this.refresh = bind(this.refresh, this); + this.keydownNoteText = bind(this.keydownNoteText, this); + this.notes_url = notes_url; + this.note_ids = note_ids; + this.last_fetched_at = last_fetched_at; + this.view = view; + this.noteable_url = document.URL; + this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); + this.basePollingInterval = 15000; + this.maxPollingSteps = 4; + this.cleanBinding(); + this.addBinding(); + this.setPollingInterval(); + this.setupMainTargetNoteForm(); + this.initTaskList(); + } + + Notes.prototype.addBinding = function() { + $(document).on("ajax:success", ".js-main-target-form", this.addNote); + $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote); + $(document).on("ajax:error", ".js-main-target-form", this.addNoteError); + $(document).on("ajax:success", "form.edit-note", this.updateNote); + $(document).on("click", ".js-note-edit", this.showEditForm); + $(document).on("click", ".note-edit-cancel", this.cancelEdit); + $(document).on("click", ".js-comment-button", this.updateCloseButton); + $(document).on("keyup input", ".js-note-text", this.updateTargetButtons); + $(document).on("click", ".js-note-delete", this.removeNote); + $(document).on("click", ".js-note-attachment-delete", this.removeAttachment); + $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton); + $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm); + $(document).on("click", ".js-note-discard", this.resetMainTargetForm); + $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); + $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote); + $(document).on("click", ".js-add-diff-note-button", this.addDiffNote); + $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); + $(document).on("visibilitychange", this.visibilityChange); + $(document).on("issuable:change", this.refresh); + return $(document).on("keydown", ".js-note-text", this.keydownNoteText); + }; + + Notes.prototype.cleanBinding = function() { + $(document).off("ajax:success", ".js-main-target-form"); + $(document).off("ajax:success", ".js-discussion-note-form"); + $(document).off("ajax:success", "form.edit-note"); + $(document).off("click", ".js-note-edit"); + $(document).off("click", ".note-edit-cancel"); + $(document).off("click", ".js-note-delete"); + $(document).off("click", ".js-note-attachment-delete"); + $(document).off("ajax:complete", ".js-main-target-form"); + $(document).off("ajax:success", ".js-main-target-form"); + $(document).off("click", ".js-discussion-reply-button"); + $(document).off("click", ".js-add-diff-note-button"); + $(document).off("visibilitychange"); + $(document).off("keyup", ".js-note-text"); + $(document).off("click", ".js-note-target-reopen"); + $(document).off("click", ".js-note-target-close"); + $(document).off("click", ".js-note-discard"); + $(document).off("keydown", ".js-note-text"); + $('.note .js-task-list-container').taskList('disable'); + return $(document).off('tasklist:changed', '.note .js-task-list-container'); + }; + + Notes.prototype.keydownNoteText = function(e) { + var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; + if (isMetaKey(e)) { + return; + } + $textarea = $(e.target); + switch (e.which) { + case 38: + if ($textarea.val() !== '') { + return; + } + myLastNote = $("li.note[data-author-id='" + gon.current_user_id + "'][data-editable]:last"); + if (myLastNote.length) { + myLastNoteEditBtn = myLastNote.find('.js-note-edit'); + return myLastNoteEditBtn.trigger('click', [true, myLastNote]); + } + break; + case 27: + discussionNoteForm = $textarea.closest('.js-discussion-note-form'); + if (discussionNoteForm.length) { + if ($textarea.val() !== '') { + if (!confirm('Are you sure you want to cancel creating this comment?')) { + return; + } + } + this.removeDiscussionNoteForm(discussionNoteForm); + return; + } + editNote = $textarea.closest('.note'); + if (editNote.length) { + originalText = $textarea.closest('form').data('original-note'); + newText = $textarea.val(); + if (originalText !== newText) { + if (!confirm('Are you sure you want to cancel editing this comment?')) { + return; + } + } + return this.removeNoteEditForm(editNote); + } + } + }; + + isMetaKey = function(e) { + return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + }; + + Notes.prototype.initRefresh = function() { + clearInterval(Notes.interval); + return Notes.interval = setInterval((function(_this) { + return function() { + return _this.refresh(); + }; + })(this), this.pollingInterval); + }; + + Notes.prototype.refresh = function() { + if (!document.hidden && document.URL.indexOf(this.noteable_url) === 0) { + return this.getContent(); + } + }; + + Notes.prototype.getContent = function() { + if (this.refreshing) { + return; + } + this.refreshing = true; + return $.ajax({ + url: this.notes_url, + data: "last_fetched_at=" + this.last_fetched_at, + dataType: "json", + success: (function(_this) { + return function(data) { + var notes; + notes = data.notes; + _this.last_fetched_at = data.last_fetched_at; + _this.setPollingInterval(data.notes.length); + return $.each(notes, function(i, note) { + if (note.discussion_html != null) { + return _this.renderDiscussionNote(note); + } else { + return _this.renderNote(note); + } + }); + }; + })(this) + }).always((function(_this) { + return function() { + return _this.refreshing = false; + }; + })(this)); + }; + + + /* + Increase @pollingInterval up to 120 seconds on every function call, + if `shouldReset` has a truthy value, 'null' or 'undefined' the variable + will reset to @basePollingInterval. + + Note: this function is used to gradually increase the polling interval + if there aren't new notes coming from the server + */ + + Notes.prototype.setPollingInterval = function(shouldReset) { + var nthInterval; + if (shouldReset == null) { + shouldReset = true; + } + nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); + if (shouldReset) { + this.pollingInterval = this.basePollingInterval; + } else if (this.pollingInterval < nthInterval) { + this.pollingInterval *= 2; + } + return this.initRefresh(); + }; + + + /* + Render note in main comments area. + + Note: for rendering inline notes use renderDiscussionNote + */ + + Notes.prototype.renderNote = function(note) { + var $notesList, votesBlock; + if (!note.valid) { + if (note.award) { + new Flash('You have already awarded this emoji!', 'alert'); + } + return; + } + if (note.award) { + votesBlock = $('.js-awards-block').eq(0); + gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name); + return gl.awardsHandler.scrollToAwards(); + } else if (this.isNewNote(note)) { + this.note_ids.push(note.id); + $notesList = $('ul.main-notes-list'); + $notesList.append(note.html).syntaxHighlight(); + gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); + this.initTaskList(); + return this.updateNotesCount(1); + } + }; + + + /* + Check if note does not exists on page + */ + + Notes.prototype.isNewNote = function(note) { + return $.inArray(note.id, this.note_ids) === -1; + }; + + Notes.prototype.isParallelView = function() { + return this.view === 'parallel'; + }; + + + /* + Render note in discussion area. + + Note: for rendering inline notes use renderDiscussionNote + */ + + Notes.prototype.renderDiscussionNote = function(note) { + var discussionContainer, form, note_html, row; + if (!this.isNewNote(note)) { + return; + } + this.note_ids.push(note.id); + form = $("#new-discussion-note-form-" + note.discussion_id); + if ((note.original_discussion_id != null) && form.length === 0) { + form = $("#new-discussion-note-form-" + note.original_discussion_id); + } + row = form.closest("tr"); + note_html = $(note.html); + note_html.syntaxHighlight(); + 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) { + row.after(note.diff_discussion_html); + row.next().find(".note").remove(); + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + discussionContainer.append(note_html); + if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { + $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); + } + } else { + discussionContainer.append(note_html); + } + gl.utils.localTimeAgo($('.js-timeago', note_html), false); + return this.updateNotesCount(1); + }; + + + /* + Called in response the main target form has been successfully submitted. + + Removes any errors. + Resets text and preview. + Resets buttons. + */ + + Notes.prototype.resetMainTargetForm = function(e) { + var form; + form = $(".js-main-target-form"); + form.find(".js-errors").remove(); + form.find(".js-md-write-button").click(); + form.find(".js-note-text").val("").trigger("input"); + form.find(".js-note-text").data("autosave").reset(); + return this.updateTargetButtons(e); + }; + + Notes.prototype.reenableTargetFormSubmitButton = function() { + var form; + form = $(".js-main-target-form"); + return form.find(".js-note-text").trigger("input"); + }; + + + /* + Shows the main form and does some setup on it. + + Sets some hidden fields in the form. + */ + + Notes.prototype.setupMainTargetNoteForm = function() { + var form; + form = $(".js-new-note-form"); + this.formClone = form.clone(); + this.setupNoteForm(form); + form.removeClass("js-new-note-form"); + form.addClass("js-main-target-form"); + form.find("#note_line_code").remove(); + form.find("#note_position").remove(); + form.find("#note_type").remove(); + return this.parentTimeline = form.parents('.timeline'); + }; + + + /* + General note form setup. + + deactivates the submit button when text is empty + hides the preview button when text is empty + setup GFM auto complete + show the form + */ + + Notes.prototype.setupNoteForm = function(form) { + var textarea; + new GLForm(form); + textarea = form.find(".js-note-text"); + return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]); + }; + + + /* + Called in response to the new note form being submitted + + Adds new note to list. + */ + + Notes.prototype.addNote = function(xhr, note, status) { + return this.renderNote(note); + }; + + Notes.prototype.addNoteError = function(xhr, note, status) { + return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline); + }; + + + /* + Called in response to the new note form being submitted + + Adds new note to list. + */ + + Notes.prototype.addDiscussionNote = function(xhr, note, status) { + this.renderDiscussionNote(note); + return this.removeDiscussionNoteForm($(xhr.target)); + }; + + + /* + Called in response to the edit note form being submitted + + Updates the current note field. + */ + + Notes.prototype.updateNote = function(_xhr, note, _status) { + var $html, $note_li; + $html = $(note.html); + gl.utils.localTimeAgo($('.js-timeago', $html)); + $html.syntaxHighlight(); + $html.find('.js-task-list-container').taskList('enable'); + $note_li = $('.note-row-' + note.id); + return $note_li.replaceWith($html); + }; + + + /* + Called in response to clicking the edit note link + + Replaces the note text with the note edit form + Adds a data attribute to the form with the original content of the note for cancellations + */ + + Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { + var $noteText, done, form, note; + e.preventDefault(); + note = $(this).closest(".note"); + note.addClass("is-editting"); + form = note.find(".note-edit-form"); + form.addClass('current-note-edit-form'); + note.find(".js-note-attachment-delete").show(); + done = function($noteText) { + var noteTextVal; + noteTextVal = $noteText.val(); + form.find('form.edit-note').data('original-note', noteTextVal); + return $noteText.val('').val(noteTextVal); + }; + new GLForm(form); + if ((scrollTo != null) && (myLastNote != null)) { + $('html, body').scrollTop($(document).height()); + return $('html, body').animate({ + scrollTop: myLastNote.offset().top - 150 + }, 500, function() { + var $noteText; + $noteText = form.find(".js-note-text"); + $noteText.focus(); + return done($noteText); + }); + } else { + $noteText = form.find('.js-note-text'); + $noteText.focus(); + return done($noteText); + } + }; + + + /* + Called in response to clicking the edit note link + + Hides edit form and restores the original note text to the editor textarea. + */ + + Notes.prototype.cancelEdit = function(e) { + var note; + e.preventDefault(); + note = $(e.target).closest('.note'); + return this.removeNoteEditForm(note); + }; + + Notes.prototype.removeNoteEditForm = function(note) { + var form; + form = note.find(".current-note-edit-form"); + note.removeClass("is-editting"); + form.removeClass("current-note-edit-form"); + return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); + }; + + + /* + Called in response to deleting a note of any kind. + + Removes the actual note from view. + Removes the whole discussion if the last note is being removed. + */ + + Notes.prototype.removeNote = function(e) { + var noteId; + noteId = $(e.currentTarget).closest(".note").attr("id"); + $(".note[id='" + noteId + "']").each((function(_this) { + return function(i, el) { + var note, notes; + note = $(el); + notes = note.closest(".notes"); + if (notes.find(".note").length === 1) { + notes.closest(".timeline-entry").remove(); + notes.closest("tr").remove(); + } + return note.remove(); + }; + })(this)); + return this.updateNotesCount(-1); + }; + + + /* + Called in response to clicking the delete attachment link + + Removes the attachment wrapper view, including image tag if it exists + Resets the note editing form + */ + + Notes.prototype.removeAttachment = function() { + var note; + note = $(this).closest(".note"); + note.find(".note-attachment").remove(); + note.find(".note-body > .note-text").show(); + note.find(".note-header").show(); + return note.find(".current-note-edit-form").remove(); + }; + + + /* + Called when clicking on the "reply" button for a diff line. + + Shows the note form below the notes. + */ + + Notes.prototype.replyToDiscussionNote = function(e) { + var form, replyLink; + form = this.formClone.clone(); + replyLink = $(e.target).closest(".js-discussion-reply-button"); + replyLink.hide(); + replyLink.after(form); + return this.setupDiscussionNoteForm(replyLink, form); + }; + + + /* + Shows the diff or discussion form and does some setup on it. + + Sets some hidden fields in the form. + + Note: dataHolder must have the "discussionId", "lineCode", "noteableType" + and "noteableId" data attributes set. + */ + + Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { + 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")); + form.find("#line_type").val(dataHolder.data("lineType")); + form.find("#note_commit_id").val(dataHolder.data("commitId")); + form.find("#note_line_code").val(dataHolder.data("lineCode")); + form.find("#note_position").val(dataHolder.attr("data-position")); + form.find("#note_noteable_type").val(dataHolder.data("noteableType")); + form.find("#note_noteable_id").val(dataHolder.data("noteableId")); + form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); + this.setupNoteForm(form); + form.find(".js-note-text").focus(); + return form.removeClass('js-main-target-form').addClass("discussion-form js-discussion-note-form"); + }; + + + /* + Called when clicking on the "add a comment" button on the side of a diff line. + + Inserts a temporary row for the form below the line. + Sets up the form and shows it. + */ + + Notes.prototype.addDiffNote = function(e) { + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent; + e.preventDefault(); + $link = $(e.currentTarget); + row = $link.closest("tr"); + nextRow = row.next(); + hasNotes = nextRow.is(".notes_holder"); + addForm = false; + targetContent = ".notes_content"; + rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>"; + if (this.isParallelView()) { + lineType = $link.data("lineType"); + targetContent += "." + lineType; + rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>"; + } + if (hasNotes) { + notesContent = nextRow.find(targetContent); + if (notesContent.length) { + replyButton = notesContent.find(".js-discussion-reply-button:visible"); + if (replyButton.length) { + e.target = replyButton[0]; + $.proxy(this.replyToDiscussionNote, replyButton[0], e).call(); + } else { + noteForm = notesContent.find(".js-discussion-note-form"); + if (noteForm.length === 0) { + addForm = true; + } + } + } + } else { + row.after(rowCssToAdd); + addForm = true; + } + if (addForm) { + newForm = this.formClone.clone(); + newForm.appendTo(row.next().find(targetContent)); + return this.setupDiscussionNoteForm($link, newForm); + } + }; + + + /* + Called in response to "cancel" on a diff note form. + + Shows the reply button again. + Removes the form and if necessary it's temporary row. + */ + + Notes.prototype.removeDiscussionNoteForm = function(form) { + var glForm, row; + row = form.closest("tr"); + glForm = form.data('gl-form'); + glForm.destroy(); + form.find(".js-note-text").data("autosave").reset(); + form.prev(".js-discussion-reply-button").show(); + if (row.is(".js-temp-notes-holder")) { + return row.remove(); + } else { + return form.remove(); + } + }; + + Notes.prototype.cancelDiscussionForm = function(e) { + var form; + e.preventDefault(); + form = $(e.target).closest(".js-discussion-note-form"); + return this.removeDiscussionNoteForm(form); + }; + + + /* + Called after an attachment file has been selected. + + Updates the file name for the selected attachment. + */ + + Notes.prototype.updateFormAttachment = function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ""); + return form.find(".js-attachment-filename").text(filename); + }; + + + /* + Called when the tab visibility changes + */ + + Notes.prototype.visibilityChange = function() { + return this.refresh(); + }; + + Notes.prototype.updateCloseButton = function(e) { + var closebtn, form, textarea; + textarea = $(e.target); + form = textarea.parents('form'); + closebtn = form.find('.js-note-target-close'); + return closebtn.text(closebtn.data('original-text')); + }; + + Notes.prototype.updateTargetButtons = function(e) { + var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea; + textarea = $(e.target); + form = textarea.parents('form'); + reopenbtn = form.find('.js-note-target-reopen'); + closebtn = form.find('.js-note-target-close'); + discardbtn = form.find('.js-note-discard'); + if (textarea.val().trim().length > 0) { + reopentext = reopenbtn.data('alternative-text'); + closetext = closebtn.data('alternative-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); + } + if (closebtn.text() !== closetext) { + closebtn.text(closetext); + } + if (reopenbtn.is(':not(.btn-comment-and-reopen)')) { + reopenbtn.addClass('btn-comment-and-reopen'); + } + if (closebtn.is(':not(.btn-comment-and-close)')) { + closebtn.addClass('btn-comment-and-close'); + } + if (discardbtn.is(':hidden')) { + return discardbtn.show(); + } + } else { + reopentext = reopenbtn.data('original-text'); + closetext = closebtn.data('original-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); + } + if (closebtn.text() !== closetext) { + closebtn.text(closetext); + } + if (reopenbtn.is('.btn-comment-and-reopen')) { + reopenbtn.removeClass('btn-comment-and-reopen'); + } + if (closebtn.is('.btn-comment-and-close')) { + closebtn.removeClass('btn-comment-and-close'); + } + if (discardbtn.is(':visible')) { + return discardbtn.hide(); + } + } + }; + + Notes.prototype.initTaskList = function() { + this.enableTaskList(); + return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList); + }; + + Notes.prototype.enableTaskList = function() { + return $('.note .js-task-list-container').taskList('enable'); + }; + + Notes.prototype.updateTaskList = function() { + return $('form', this).submit(); + }; + + Notes.prototype.updateNotesCount = function(updateCount) { + return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount); + }; + + return Notes; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee deleted file mode 100644 index 0ea54faae1a..00000000000 --- a/app/assets/javascripts/notes.js.coffee +++ /dev/null @@ -1,694 +0,0 @@ -#= require autosave -#= require autosize -#= require dropzone -#= require dropzone_input -#= require gfm_auto_complete -#= require jquery.atwho -#= require task_list - -class @Notes - @interval: null - - constructor: (notes_url, note_ids, last_fetched_at, view) -> - @notes_url = notes_url - @note_ids = note_ids - @last_fetched_at = last_fetched_at - @view = view - @noteable_url = document.URL - @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge") - @basePollingInterval = 15000 - @maxPollingSteps = 4 - - @cleanBinding() - @addBinding() - @setPollingInterval() - @setupMainTargetNoteForm() - @initTaskList() - - addBinding: -> - # add note to UI after creation - $(document).on "ajax:success", ".js-main-target-form", @addNote - $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote - - # catch note ajax errors - $(document).on "ajax:error", ".js-main-target-form", @addNoteError - - # change note in UI after update - $(document).on "ajax:success", "form.edit-note", @updateNote - - # Edit note link - $(document).on "click", ".js-note-edit", @showEditForm - $(document).on "click", ".note-edit-cancel", @cancelEdit - - # Reopen and close actions for Issue/MR combined with note form submit - $(document).on "click", ".js-comment-button", @updateCloseButton - $(document).on "keyup input", ".js-note-text", @updateTargetButtons - - # remove a note (in general) - $(document).on "click", ".js-note-delete", @removeNote - - # delete note attachment - $(document).on "click", ".js-note-attachment-delete", @removeAttachment - - # reset main target form after submit - $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton - $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm - - # reset main target form when clicking discard - $(document).on "click", ".js-note-discard", @resetMainTargetForm - - # update the file name when an attachment is selected - $(document).on "change", ".js-note-attachment-input", @updateFormAttachment - - # reply to diff/discussion notes - $(document).on "click", ".js-discussion-reply-button", @replyToDiscussionNote - - # add diff note - $(document).on "click", ".js-add-diff-note-button", @addDiffNote - - # hide diff note form - $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm - - # fetch notes when tab becomes visible - $(document).on "visibilitychange", @visibilityChange - - # when issue status changes, we need to refresh data - $(document).on "issuable:change", @refresh - - # when a key is clicked on the notes - $(document).on "keydown", ".js-note-text", @keydownNoteText - - cleanBinding: -> - $(document).off "ajax:success", ".js-main-target-form" - $(document).off "ajax:success", ".js-discussion-note-form" - $(document).off "ajax:success", "form.edit-note" - $(document).off "click", ".js-note-edit" - $(document).off "click", ".note-edit-cancel" - $(document).off "click", ".js-note-delete" - $(document).off "click", ".js-note-attachment-delete" - $(document).off "ajax:complete", ".js-main-target-form" - $(document).off "ajax:success", ".js-main-target-form" - $(document).off "click", ".js-discussion-reply-button" - $(document).off "click", ".js-add-diff-note-button" - $(document).off "visibilitychange" - $(document).off "keyup", ".js-note-text" - $(document).off "click", ".js-note-target-reopen" - $(document).off "click", ".js-note-target-close" - $(document).off "click", ".js-note-discard" - $(document).off "keydown", ".js-note-text" - - $('.note .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.note .js-task-list-container' - - keydownNoteText: (e) => - return if isMetaKey e - - $textarea = $(e.target) - - # Edit previous note when UP arrow is hit - switch e.which - when 38 - return unless $textarea.val() is '' - - myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last") - if myLastNote.length - myLastNoteEditBtn = myLastNote.find('.js-note-edit') - myLastNoteEditBtn.trigger('click', [true, myLastNote]) - - # Cancel creating diff note or editing any note when ESCAPE is hit - when 27 - discussionNoteForm = $textarea.closest('.js-discussion-note-form') - if discussionNoteForm.length - if $textarea.val() isnt '' - return unless confirm('Are you sure you want to cancel creating this comment?') - - @removeDiscussionNoteForm(discussionNoteForm) - return - - editNote = $textarea.closest('.note') - if editNote.length - originalText = $textarea.closest('form').data('original-note') - newText = $textarea.val() - if originalText isnt newText - return unless confirm('Are you sure you want to cancel editing this comment?') - - @removeNoteEditForm(editNote) - - - isMetaKey = (e) -> - (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey) - - initRefresh: -> - clearInterval(Notes.interval) - Notes.interval = setInterval => - @refresh() - , @pollingInterval - - refresh: => - if not document.hidden and document.URL.indexOf(@noteable_url) is 0 - @getContent() - - getContent: -> - return if @refreshing - - @refreshing = true - - $.ajax - url: @notes_url - data: "last_fetched_at=" + @last_fetched_at - dataType: "json" - success: (data) => - notes = data.notes - @last_fetched_at = data.last_fetched_at - @setPollingInterval(data.notes.length) - $.each notes, (i, note) => - if note.discussion_with_diff_html? - @renderDiscussionNote(note) - else - @renderNote(note) - .always () => - @refreshing = false - - ### - Increase @pollingInterval up to 120 seconds on every function call, - if `shouldReset` has a truthy value, 'null' or 'undefined' the variable - will reset to @basePollingInterval. - - Note: this function is used to gradually increase the polling interval - if there aren't new notes coming from the server - ### - setPollingInterval: (shouldReset = true) -> - nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1) - if shouldReset - @pollingInterval = @basePollingInterval - else if @pollingInterval < nthInterval - @pollingInterval *= 2 - - @initRefresh() - - ### - Render note in main comments area. - - Note: for rendering inline notes use renderDiscussionNote - ### - renderNote: (note) -> - unless note.valid - if note.award - new Flash('You have already awarded this emoji!', 'alert') - return - - if note.award - votesBlock = $('.js-awards-block').eq 0 - gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name - gl.awardsHandler.scrollToAwards() - - # render note if it not present in loaded list - # or skip if rendered - else if @isNewNote(note) - @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) - - @initTaskList() - @updateNotesCount(1) - - - ### - Check if note does not exists on page - ### - isNewNote: (note) -> - $.inArray(note.id, @note_ids) == -1 - - isParallelView: -> - @view == 'parallel' - - ### - Render note in discussion area. - - Note: for rendering inline notes use renderDiscussionNote - ### - renderDiscussionNote: (note) -> - return unless @isNewNote(note) - - @note_ids.push(note.id) - form = $("#new-discussion-note-form-#{note.discussion_id}") - if note.original_discussion_id? and form.length is 0 - form = $("#new-discussion-note-form-#{note.original_discussion_id}") - 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? and discussionContainer.length is 0 - discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']") - if discussionContainer.length is 0 - # insert the note and the reply button after the temp row - row.after note.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') is 0 - $('ul.main-notes-list') - .append(note.discussion_with_diff_html) - .syntaxHighlight() - else - # append new note to all matching discussions - discussionContainer.append note_html - - gl.utils.localTimeAgo($('.js-timeago', note_html), false) - - @updateNotesCount(1) - - ### - Called in response the main target form has been successfully submitted. - - Removes any errors. - Resets text and preview. - Resets buttons. - ### - resetMainTargetForm: (e) => - 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() - - @updateTargetButtons(e) - - reenableTargetFormSubmitButton: -> - form = $(".js-main-target-form") - - form.find(".js-note-text").trigger "input" - - ### - Shows the main form and does some setup on it. - - Sets some hidden fields in the form. - ### - setupMainTargetNoteForm: -> - # find the form - form = $(".js-new-note-form") - - # Set a global clone of the form for later cloning - @formClone = form.clone() - - # show the form - @setupNoteForm(form) - - # fix classes - form.removeClass "js-new-note-form" - form.addClass "js-main-target-form" - - form.find("#note_line_code").remove() - form.find("#note_position").remove() - form.find("#note_type").remove() - - @parentTimeline = form.parents('.timeline') - - ### - General note form setup. - - deactivates the submit button when text is empty - hides the preview button when text is empty - setup GFM auto complete - show the form - ### - setupNoteForm: (form) -> - new GLForm form - - textarea = form.find(".js-note-text") - - new Autosave textarea, [ - "Note" - form.find("#note_noteable_type").val() - form.find("#note_noteable_id").val() - form.find("#note_commit_id").val() - form.find("#note_type").val() - form.find("#note_line_code").val() - form.find("#note_position").val() - ] - - ### - Called in response to the new note form being submitted - - Adds new note to list. - ### - addNote: (xhr, note, status) => - @renderNote(note) - - addNoteError: (xhr, note, status) => - new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', @parentTimeline) - - ### - Called in response to the new note form being submitted - - Adds new note to list. - ### - addDiscussionNote: (xhr, note, status) => - @renderDiscussionNote(note) - - # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm($(xhr.target)) - - ### - Called in response to the edit note form being submitted - - Updates the current note field. - ### - updateNote: (_xhr, note, _status) => - # 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) - - ### - Called in response to clicking the edit note link - - Replaces the note text with the note edit form - Adds a data attribute to the form with the original content of the note for cancellations - ### - showEditForm: (e, scrollTo, myLastNote) -> - e.preventDefault() - note = $(this).closest(".note") - 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 = ($noteText) -> - # 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 - $noteText.val('').val(noteTextVal); - - new GLForm form - if scrollTo? and myLastNote? - # scroll to the bottom - # so the open of the last element doesn't make a jump - $('html, body').scrollTop($(document).height()); - $('html, body').animate({ - scrollTop: myLastNote.offset().top - 150 - }, 500, -> - $noteText = form.find(".js-note-text") - $noteText.focus() - done($noteText) - ); - else - $noteText = form.find('.js-note-text') - $noteText.focus() - done($noteText) - - ### - Called in response to clicking the edit note link - - Hides edit form and restores the original note text to the editor textarea. - ### - cancelEdit: (e) => - e.preventDefault() - note = $(e.target).closest('.note') - @removeNoteEditForm(note) - - removeNoteEditForm: (note) -> - 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. - form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')) - - ### - Called in response to deleting a note of any kind. - - Removes the actual note from view. - Removes the whole discussion if the last note is being removed. - ### - removeNote: (e) => - noteId = $(e.currentTarget) - .closest(".note") - .attr("id") - - # 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. - $(".note[id='#{noteId}']").each (i, el) => - note = $(el) - notes = note.closest(".notes") - - # check if this is the last note for this line - if notes.find(".note").length is 1 - - # "Discussions" tab - notes.closest(".timeline-entry").remove() - - # "Changes" tab / commit view - notes.closest("tr").remove() - - note.remove() - - # Decrement the "Discussions" counter only once - @updateNotesCount(-1) - - ### - Called in response to clicking the delete attachment link - - Removes the attachment wrapper view, including image tag if it exists - Resets the note editing form - ### - removeAttachment: -> - note = $(this).closest(".note") - note.find(".note-attachment").remove() - note.find(".note-body > .note-text").show() - note.find(".note-header").show() - note.find(".current-note-edit-form").remove() - - ### - Called when clicking on the "reply" button for a diff line. - - Shows the note form below the notes. - ### - replyToDiscussionNote: (e) => - form = @formClone.clone() - replyLink = $(e.target).closest(".js-discussion-reply-button") - replyLink.hide() - - # insert the form after the button - replyLink.after form - - # show the form - @setupDiscussionNoteForm(replyLink, form) - - ### - Shows the diff or discussion form and does some setup on it. - - Sets some hidden fields in the form. - - Note: dataHolder must have the "discussionId", "lineCode", "noteableType" - and "noteableId" data attributes set. - ### - setupDiscussionNoteForm: (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") - form.find("#line_type").val dataHolder.data("lineType") - form.find("#note_commit_id").val dataHolder.data("commitId") - form.find("#note_line_code").val dataHolder.data("lineCode") - form.find("#note_position").val dataHolder.attr("data-position") - form.find("#note_noteable_type").val dataHolder.data("noteableType") - form.find("#note_noteable_id").val dataHolder.data("noteableId") - form.find('.js-note-discard') - .show() - .removeClass('js-note-discard') - .addClass('js-close-discussion-note-form') - .text(form.find('.js-close-discussion-note-form').data('cancel-text')) - @setupNoteForm form - form.find(".js-note-text").focus() - form - .removeClass('js-main-target-form') - .addClass("discussion-form js-discussion-note-form") - - ### - Called when clicking on the "add a comment" button on the side of a diff line. - - Inserts a temporary row for the form below the line. - Sets up the form and shows it. - ### - addDiffNote: (e) => - e.preventDefault() - $link = $(e.currentTarget) - row = $link.closest("tr") - nextRow = row.next() - hasNotes = nextRow.is(".notes_holder") - addForm = false - targetContent = ".notes_content" - rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>" - - # In parallel view, look inside the correct left/right pane - if @isParallelView() - lineType = $link.data("lineType") - targetContent += "." + lineType - rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>" - - if hasNotes - notesContent = nextRow.find(targetContent) - if notesContent.length - replyButton = notesContent.find(".js-discussion-reply-button:visible") - if replyButton.length - e.target = replyButton[0] - $.proxy(@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 - else - # add a notes row and insert the form - row.after rowCssToAdd - addForm = true - - if addForm - newForm = @formClone.clone() - newForm.appendTo row.next().find(targetContent) - - # show the form - @setupDiscussionNoteForm $link, newForm - - ### - Called in response to "cancel" on a diff note form. - - Shows the reply button again. - Removes the form and if necessary it's temporary row. - ### - removeDiscussionNoteForm: (form)-> - row = form.closest("tr") - - 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(".js-discussion-reply-button").show() - if row.is(".js-temp-notes-holder") - # remove temporary row for diff lines - row.remove() - else - # only remove the form - form.remove() - - cancelDiscussionForm: (e) => - e.preventDefault() - form = $(e.target).closest(".js-discussion-note-form") - @removeDiscussionNoteForm(form) - - ### - Called after an attachment file has been selected. - - Updates the file name for the selected attachment. - ### - updateFormAttachment: -> - form = $(this).closest("form") - - # get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, "") - form.find(".js-attachment-filename").text filename - - ### - Called when the tab visibility changes - ### - visibilityChange: => - @refresh() - - updateCloseButton: (e) => - textarea = $(e.target) - form = textarea.parents('form') - closebtn = form.find('.js-note-target-close') - closebtn.text(closebtn.data('original-text')) - - updateTargetButtons: (e) => - textarea = $(e.target) - form = textarea.parents('form') - reopenbtn = form.find('.js-note-target-reopen') - closebtn = form.find('.js-note-target-close') - discardbtn = form.find('.js-note-discard') - - if textarea.val().trim().length > 0 - reopentext = reopenbtn.data('alternative-text') - closetext = closebtn.data('alternative-text') - - if reopenbtn.text() isnt reopentext - reopenbtn.text(reopentext) - - if closebtn.text() isnt closetext - closebtn.text(closetext) - - if reopenbtn.is(':not(.btn-comment-and-reopen)') - reopenbtn.addClass('btn-comment-and-reopen') - - if closebtn.is(':not(.btn-comment-and-close)') - closebtn.addClass('btn-comment-and-close') - - if discardbtn.is(':hidden') - discardbtn.show() - else - reopentext = reopenbtn.data('original-text') - closetext = closebtn.data('original-text') - - if reopenbtn.text() isnt reopentext - reopenbtn.text(reopentext) - - if closebtn.text() isnt closetext - closebtn.text(closetext) - - if reopenbtn.is('.btn-comment-and-reopen') - reopenbtn.removeClass('btn-comment-and-reopen') - - if closebtn.is('.btn-comment-and-close') - closebtn.removeClass('btn-comment-and-close') - - if discardbtn.is(':visible') - discardbtn.hide() - - initTaskList: -> - @enableTaskList() - $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList - - enableTaskList: -> - $('.note .js-task-list-container').taskList('enable') - - updateTaskList: -> - $('form', this).submit() - - updateNotesCount: (updateCount) -> - @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount) diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js new file mode 100644 index 00000000000..a41e9d3fabe --- /dev/null +++ b/app/assets/javascripts/notifications_dropdown.js @@ -0,0 +1,30 @@ +(function() { + this.NotificationsDropdown = (function() { + function NotificationsDropdown() { + $(document).off('click', '.update-notification').on('click', '.update-notification', function(e) { + var form, label, notificationLevel; + e.preventDefault(); + if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') { + return; + } + notificationLevel = $(this).data('notification-level'); + label = $(this).data('notification-title'); + form = $(this).parents('.notification-form:first'); + form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner'); + form.find('#notification_setting_level').val(notificationLevel); + return form.submit(); + }); + $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) { + if (data.saved) { + return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html); + } else { + return new Flash('Failed to save new settings', 'alert'); + } + }); + } + + return NotificationsDropdown; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee deleted file mode 100644 index 0bbd082c156..00000000000 --- a/app/assets/javascripts/notifications_dropdown.js.coffee +++ /dev/null @@ -1,25 +0,0 @@ -class @NotificationsDropdown - constructor: -> - $(document) - .off 'click', '.update-notification' - .on 'click', '.update-notification', (e) -> - e.preventDefault() - - return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom' - - notificationLevel = $(@).data 'notification-level' - label = $(@).data 'notification-title' - form = $(this).parents('.notification-form:first') - form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner' - form.find('#notification_setting_level').val(notificationLevel) - form.submit() - - $(document) - .off 'ajax:success', '.notification-form' - .on 'ajax:success', '.notification-form', (e, data) -> - if data.saved - $(e.currentTarget) - .closest('.notification-dropdown') - .replaceWith(data.html) - else - new Flash('Failed to save new settings', 'alert') diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js new file mode 100644 index 00000000000..6b2ef17ef6b --- /dev/null +++ b/app/assets/javascripts/notifications_form.js @@ -0,0 +1,58 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.NotificationsForm = (function() { + function NotificationsForm() { + this.toggleCheckbox = bind(this.toggleCheckbox, this); + this.removeEventListeners(); + this.initEventListeners(); + } + + NotificationsForm.prototype.removeEventListeners = function() { + return $(document).off('change', '.js-custom-notification-event'); + }; + + NotificationsForm.prototype.initEventListeners = function() { + return $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox); + }; + + NotificationsForm.prototype.toggleCheckbox = function(e) { + var $checkbox, $parent; + $checkbox = $(e.currentTarget); + $parent = $checkbox.closest('.checkbox'); + return this.saveEvent($checkbox, $parent); + }; + + NotificationsForm.prototype.showCheckboxLoadingSpinner = function($parent) { + return $parent.addClass('is-loading').find('.custom-notification-event-loading').removeClass('fa-check').addClass('fa-spin fa-spinner').removeClass('is-done'); + }; + + NotificationsForm.prototype.saveEvent = function($checkbox, $parent) { + var form; + form = $parent.parents('form:first'); + return $.ajax({ + url: form.attr('action'), + method: form.attr('method'), + dataType: 'json', + data: form.serialize(), + beforeSend: (function(_this) { + return function() { + return _this.showCheckboxLoadingSpinner($parent); + }; + })(this) + }).done(function(data) { + $checkbox.enable(); + if (data.saved) { + $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + return setTimeout(function() { + return $parent.removeClass('is-loading').find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + }, 2000); + } + }); + }; + + return NotificationsForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee deleted file mode 100644 index 3432428702a..00000000000 --- a/app/assets/javascripts/notifications_form.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -class @NotificationsForm - constructor: -> - @removeEventListeners() - @initEventListeners() - - removeEventListeners: -> - $(document).off 'change', '.js-custom-notification-event' - - initEventListeners: -> - $(document).on 'change', '.js-custom-notification-event', @toggleCheckbox - - toggleCheckbox: (e) => - $checkbox = $(e.currentTarget) - $parent = $checkbox.closest('.checkbox') - @saveEvent($checkbox, $parent) - - showCheckboxLoadingSpinner: ($parent) -> - $parent - .addClass 'is-loading' - .find '.custom-notification-event-loading' - .removeClass 'fa-check' - .addClass 'fa-spin fa-spinner' - .removeClass 'is-done' - - saveEvent: ($checkbox, $parent) -> - form = $parent.parents('form:first') - - $.ajax( - url: form.attr('action') - method: form.attr('method') - dataType: 'json' - data: form.serialize() - - beforeSend: => - @showCheckboxLoadingSpinner($parent) - ).done (data) -> - $checkbox.enable() - - if data.saved - $parent - .find '.custom-notification-event-loading' - .toggleClass 'fa-spin fa-spinner fa-check is-done' - - setTimeout(-> - $parent - .removeClass 'is-loading' - .find '.custom-notification-event-loading' - .toggleClass 'fa-spin fa-spinner fa-check is-done' - , 2000) diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js new file mode 100644 index 00000000000..b81ed50cb48 --- /dev/null +++ b/app/assets/javascripts/pager.js @@ -0,0 +1,63 @@ +(function() { + this.Pager = { + init: function(limit, preload, disable, callback) { + this.limit = limit != null ? limit : 0; + this.disable = disable != null ? disable : false; + this.callback = callback != null ? callback : $.noop; + this.loading = $('.loading').first(); + if (preload) { + this.offset = 0; + this.getOld(); + } else { + this.offset = this.limit; + } + return this.initLoadMore(); + }, + getOld: function() { + this.loading.show(); + return $.ajax({ + type: "GET", + url: $(".content_list").data('href') || location.href, + data: "limit=" + this.limit + "&offset=" + this.offset, + complete: (function(_this) { + return function() { + return _this.loading.hide(); + }; + })(this), + success: function(data) { + Pager.append(data.count, data.html); + return Pager.callback(); + }, + dataType: "json" + }); + }, + append: function(count, html) { + $(".content_list").append(html); + if (count > 0) { + return this.offset += count; + } else { + return this.disable = true; + } + }, + initLoadMore: function() { + $(document).unbind('scroll'); + return $(document).endlessScroll({ + bottomPixels: 400, + fireDelay: 1000, + fireOnce: true, + ceaseFire: function() { + return Pager.disable; + }, + callback: (function(_this) { + return function(i) { + if (!_this.loading.is(':visible')) { + _this.loading.show(); + return Pager.getOld(); + } + }; + })(this) + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee deleted file mode 100644 index 8049c5c30e2..00000000000 --- a/app/assets/javascripts/pager.js.coffee +++ /dev/null @@ -1,44 +0,0 @@ -@Pager = - init: (@limit = 0, preload, @disable = false, @callback = $.noop) -> - @loading = $('.loading').first() - - if preload - @offset = 0 - @getOld() - else - @offset = @limit - @initLoadMore() - - getOld: -> - @loading.show() - $.ajax - type: "GET" - url: $(".content_list").data('href') || location.href - data: "limit=" + @limit + "&offset=" + @offset - complete: => - @loading.hide() - success: (data) -> - Pager.append(data.count, data.html) - Pager.callback() - dataType: "json" - - append: (count, html) -> - $(".content_list").append html - if count > 0 - @offset += count - else - @disable = true - - initLoadMore: -> - $(document).unbind('scroll') - $(document).endlessScroll - bottomPixels: 400 - fireDelay: 1000 - fireOnce: true - ceaseFire: -> - Pager.disable - - callback: (i) => - unless @loading.is(':visible') - @loading.show() - Pager.getOld() diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js new file mode 100644 index 00000000000..a3eea316f67 --- /dev/null +++ b/app/assets/javascripts/profile/gl_crop.js @@ -0,0 +1,169 @@ +(function() { + var GitLabCrop, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + GitLabCrop = (function() { + var FILENAMEREGEX; + + FILENAMEREGEX = /^.*[\\\/]/; + + function GitLabCrop(input, opts) { + var ref, ref1, ref2, ref3, ref4; + if (opts == null) { + opts = {}; + } + this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this); + this.onModalHide = bind(this.onModalHide, this); + this.onModalShow = bind(this.onModalShow, this); + this.onPickImageClick = bind(this.onPickImageClick, this); + this.fileInput = $(input); + this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger"); + 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; + this.filename = this.getElement(this.filename); + this.previewImage = this.getElement(this.previewImage); + this.pickImageEl = this.getElement(this.pickImageEl); + 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; + this.cropActionsBtn = this.modalCrop.find('[data-method]'); + this.bindEvents(); + } + + GitLabCrop.prototype.getElement = function(selector) { + return $(selector, this.form); + }; + + GitLabCrop.prototype.bindEvents = function() { + var _this; + _this = this; + this.fileInput.on('change', function(e) { + return _this.onFileInputChange(e, this); + }); + this.pickImageEl.on('click', this.onPickImageClick); + this.modalCrop.on('shown.bs.modal', this.onModalShow); + this.modalCrop.on('hidden.bs.modal', this.onModalHide); + this.uploadImageBtn.on('click', this.onUploadImageBtnClick); + this.cropActionsBtn.on('click', function(e) { + var btn; + btn = this; + return _this.onActionBtnClick(btn); + }); + return this.croppedImageBlob = null; + }; + + GitLabCrop.prototype.onPickImageClick = function() { + return this.fileInput.trigger('click'); + }; + + GitLabCrop.prototype.onModalShow = function() { + var _this; + _this = this; + return this.modalCropImg.cropper({ + viewMode: 1, + center: false, + aspectRatio: 1, + modal: true, + scalable: false, + rotatable: false, + zoomable: true, + dragMode: 'move', + guides: false, + zoomOnTouch: false, + zoomOnWheel: false, + cropBoxMovable: false, + cropBoxResizable: false, + toggleDragModeOnDblclick: false, + built: function() { + var $image, container, cropBoxHeight, cropBoxWidth; + $image = $(this); + container = $image.cropper('getContainerData'); + cropBoxWidth = _this.cropBoxWidth; + cropBoxHeight = _this.cropBoxHeight; + return $image.cropper('setCropBoxData', { + width: cropBoxWidth, + height: cropBoxHeight, + left: (container.width - cropBoxWidth) / 2, + top: (container.height - cropBoxHeight) / 2 + }); + } + }); + }; + + GitLabCrop.prototype.onModalHide = function() { + return this.modalCropImg.attr('src', '').cropper('destroy'); + }; + + GitLabCrop.prototype.onUploadImageBtnClick = function(e) { + e.preventDefault(); + this.setBlob(); + this.setPreview(); + this.modalCrop.modal('hide'); + return this.fileInput.val(''); + }; + + GitLabCrop.prototype.onActionBtnClick = function(btn) { + var data, result; + data = $(btn).data(); + if (this.modalCropImg.data('cropper') && data.method) { + return result = this.modalCropImg.cropper(data.method, data.option); + } + }; + + GitLabCrop.prototype.onFileInputChange = function(e, input) { + return this.readFile(input); + }; + + GitLabCrop.prototype.readFile = function(input) { + var _this, reader; + _this = this; + reader = new FileReader; + reader.onload = function() { + _this.modalCropImg.attr('src', reader.result); + return _this.modalCrop.modal('show'); + }; + return reader.readAsDataURL(input.files[0]); + }; + + GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { + var array, binary, i, k, len, v; + binary = atob(dataURL.split(',')[1]); + array = []; + for (k = i = 0, len = binary.length; i < len; k = ++i) { + v = binary[k]; + array.push(binary.charCodeAt(k)); + } + return new Blob([new Uint8Array(array)], { + type: 'image/png' + }); + }; + + GitLabCrop.prototype.setPreview = function() { + var filename; + this.previewImage.attr('src', this.dataURL); + filename = this.fileInput.val().replace(FILENAMEREGEX, ''); + return this.filename.text(filename); + }; + + GitLabCrop.prototype.setBlob = function() { + this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { + width: 200, + height: 200 + }).toDataURL('image/png'); + return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); + }; + + GitLabCrop.prototype.getBlob = function() { + return this.croppedImageBlob; + }; + + return GitLabCrop; + + })(); + + $.fn.glCrop = function(opts) { + return this.each(function() { + return $(this).data('glcrop', new GitLabCrop(this, opts)); + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/profile/gl_crop.js.coffee b/app/assets/javascripts/profile/gl_crop.js.coffee deleted file mode 100644 index df9bfdfa6cc..00000000000 --- a/app/assets/javascripts/profile/gl_crop.js.coffee +++ /dev/null @@ -1,152 +0,0 @@ -class GitLabCrop - # Matches everything but the file name - FILENAMEREGEX = /^.*[\\\/]/ - - constructor: (input, opts = {}) -> - @fileInput = $(input) - - # We should rename to avoid spec to fail - # Form will submit the proper input filed with a file using FormData - @fileInput - .attr('name', "#{@fileInput.attr('name')}-trigger") - .attr('id', "#{@fileInput.attr('id')}-trigger") - - # Set defaults - { - @exportWidth = 200 - @exportHeight = 200 - @cropBoxWidth = 200 - @cropBoxHeight = 200 - @form = @fileInput.parents('form') - - # Required params - @filename - @previewImage - @modalCrop - @pickImageEl - @uploadImageBtn - @modalCropImg - } = opts - - # Ensure needed elements are jquery objects - # If selector is provided we will convert them to a jQuery Object - @filename = @getElement(@filename) - @previewImage = @getElement(@previewImage) - @pickImageEl = @getElement(@pickImageEl) - - # Modal elements usually are outside the @form element - @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop - @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn - @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg - - @cropActionsBtn = @modalCrop.find('[data-method]') - - @bindEvents() - - getElement: (selector) -> - $(selector, @form) - - bindEvents: -> - _this = @ - @fileInput.on 'change', (e) -> - _this.onFileInputChange(e, @) - - @pickImageEl.on 'click', @onPickImageClick - @modalCrop.on 'shown.bs.modal', @onModalShow - @modalCrop.on 'hidden.bs.modal', @onModalHide - @uploadImageBtn.on 'click', @onUploadImageBtnClick - @cropActionsBtn.on 'click', (e) -> - btn = @ - _this.onActionBtnClick(btn) - @croppedImageBlob = null - - onPickImageClick: => - @fileInput.trigger('click') - - onModalShow: => - _this = @ - @modalCropImg.cropper( - viewMode: 1 - center: false - aspectRatio: 1 - modal: true - scalable: false - rotatable: false - zoomable: true - dragMode: 'move' - guides: false - zoomOnTouch: false - zoomOnWheel: false - cropBoxMovable: false - cropBoxResizable: false - toggleDragModeOnDblclick: false - built: -> - $image = $(@) - container = $image.cropper 'getContainerData' - cropBoxWidth = _this.cropBoxWidth; - cropBoxHeight = _this.cropBoxHeight; - - $image.cropper('setCropBoxData', - width: cropBoxWidth, - height: cropBoxHeight, - left: (container.width - cropBoxWidth) / 2, - top: (container.height - cropBoxHeight) / 2 - ) - ) - - - onModalHide: => - @modalCropImg - .attr('src', '') # Remove attached image - .cropper('destroy') # Destroy cropper instance - - onUploadImageBtnClick: (e) => - e.preventDefault() - @setBlob() - @setPreview() - @modalCrop.modal('hide') - @fileInput.val('') - - onActionBtnClick: (btn) -> - data = $(btn).data() - - if @modalCropImg.data('cropper') && data.method - result = @modalCropImg.cropper data.method, data.option - - onFileInputChange: (e, input) -> - @readFile(input) - - readFile: (input) -> - _this = @ - reader = new FileReader - reader.onload = -> - _this.modalCropImg.attr('src', reader.result) - _this.modalCrop.modal('show') - - reader.readAsDataURL(input.files[0]) - - dataURLtoBlob: (dataURL) -> - binary = atob(dataURL.split(',')[1]) - array = [] - for v, k in binary - array.push(binary.charCodeAt(k)) - new Blob([new Uint8Array(array)], type: 'image/png') - - setPreview: -> - @previewImage.attr('src', @dataURL) - filename = @fileInput.val().replace(FILENAMEREGEX, '') - @filename.text(filename) - - setBlob: -> - @dataURL = @modalCropImg.cropper('getCroppedCanvas', - width: 200 - height: 200 - ).toDataURL('image/png') - @croppedImageBlob = @dataURLtoBlob(@dataURL) - - getBlob: -> - @croppedImageBlob - -$.fn.glCrop = (opts) -> - return @.each -> - $(@).data('glcrop', new GitLabCrop(@, opts)) diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js new file mode 100644 index 00000000000..ed1d87abafe --- /dev/null +++ b/app/assets/javascripts/profile/profile.js @@ -0,0 +1,102 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Profile = (function() { + function Profile(opts) { + var cropOpts, ref; + if (opts == null) { + opts = {}; + } + this.onSubmitForm = bind(this.onSubmitForm, this); + this.form = (ref = opts.form) != null ? ref : $('.edit-user'); + $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() { + return $(this).parents('form').submit(); + }); + $('#user_notification_email').on('change', function() { + return $(this).parents('form').submit(); + }); + $('.update-username').on('ajax:before', function() { + $('.loading-username').show(); + $(this).find('.update-success').hide(); + return $(this).find('.update-failed').hide(); + }); + $('.update-username').on('ajax:complete', function() { + $('.loading-username').hide(); + $(this).find('.btn-save').enable(); + return $(this).find('.loading-gif').hide(); + }); + $('.update-notifications').on('ajax:success', function(e, data) { + if (data.saved) { + return new Flash("Notification settings saved", "notice"); + } else { + return new Flash("Failed to save new settings", "alert"); + } + }); + this.bindEvents(); + cropOpts = { + filename: '.js-avatar-filename', + previewImage: '.avatar-image .avatar', + modalCrop: '.modal-profile-crop', + pickImageEl: '.js-choose-user-avatar-button', + uploadImageBtn: '.js-upload-user-avatar', + modalCropImg: '.modal-profile-crop-image' + }; + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + } + + Profile.prototype.bindEvents = function() { + return this.form.on('submit', this.onSubmitForm); + }; + + Profile.prototype.onSubmitForm = function(e) { + e.preventDefault(); + return this.saveForm(); + }; + + Profile.prototype.saveForm = function() { + var avatarBlob, formData, self; + self = this; + formData = new FormData(this.form[0]); + avatarBlob = this.avatarGlCrop.getBlob(); + if (avatarBlob != null) { + formData.append('user[avatar]', avatarBlob, 'avatar.png'); + } + return $.ajax({ + url: this.form.attr('action'), + type: this.form.attr('method'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + success: function(response) { + return new Flash(response.message, 'notice'); + }, + error: function(jqXHR) { + return new Flash(jqXHR.responseJSON.message, 'alert'); + }, + complete: function() { + window.scrollTo(0, 0); + return self.form.find(':input[disabled]').enable(); + } + }); + }; + + return Profile; + + })(); + + $(function() { + $(document).on('focusout.ssh_key', '#key_key', function() { + var $title, comment; + $title = $('#key_title'); + comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + if (comment && comment.length > 1 && $title.val() === '') { + return $title.val(comment[1]).change(); + } + }); + if (gl.utils.getPagePath() === 'profiles') { + return new Profile(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/profile/profile.js.coffee b/app/assets/javascripts/profile/profile.js.coffee deleted file mode 100644 index f3b05f2c646..00000000000 --- a/app/assets/javascripts/profile/profile.js.coffee +++ /dev/null @@ -1,83 +0,0 @@ -class @Profile - constructor: (opts = {}) -> - { - @form = $('.edit-user') - } = opts - - # Automatically submit the Preferences form when any of its radio buttons change - $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> - $(this).parents('form').submit() - - # Automatically submit email form when it changes - $('#user_notification_email').on 'change', -> - $(this).parents('form').submit() - - $('.update-username').on 'ajax:before', -> - $('.loading-username').show() - $(this).find('.update-success').hide() - $(this).find('.update-failed').hide() - - $('.update-username').on 'ajax:complete', -> - $('.loading-username').hide() - $(this).find('.btn-save').enable() - $(this).find('.loading-gif').hide() - - $('.update-notifications').on 'ajax:success', (e, data) -> - if data.saved - new Flash("Notification settings saved", "notice") - else - new Flash("Failed to save new settings", "alert") - - @bindEvents() - - cropOpts = - filename: '.js-avatar-filename' - previewImage: '.avatar-image .avatar' - modalCrop: '.modal-profile-crop' - pickImageEl: '.js-choose-user-avatar-button' - uploadImageBtn: '.js-upload-user-avatar' - modalCropImg: '.modal-profile-crop-image' - - @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop' - - bindEvents: -> - @form.on 'submit', @onSubmitForm - - onSubmitForm: (e) => - e.preventDefault() - @saveForm() - - saveForm: -> - self = @ - formData = new FormData(@form[0]) - - avatarBlob = @avatarGlCrop.getBlob() - formData.append('user[avatar]', avatarBlob, 'avatar.png') if avatarBlob? - - $.ajax - url: @form.attr('action') - type: @form.attr('method') - data: formData - dataType: "json" - processData: false - contentType: false - success: (response) -> - new Flash(response.message, 'notice') - error: (jqXHR) -> - new Flash(jqXHR.responseJSON.message, 'alert') - complete: -> - window.scrollTo 0, 0 - # Enable submit button after requests ends - self.form.find(':input[disabled]').enable() - -$ -> - # Extract the SSH Key title from its comment - $(document).on 'focusout.ssh_key', '#key_key', -> - $title = $('#key_title') - comment = $(@).val().match(/^\S+ \S+ (.+)\n?$/) - - if comment && comment.length > 1 && $title.val() == '' - $title.val(comment[1]).change() - - if gl.utils.getPagePath() == 'profiles' - new Profile() diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js new file mode 100644 index 00000000000..b95faadc8e7 --- /dev/null +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -0,0 +1,7 @@ + +/*= require_tree . */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/profile/profile_bundle.js.coffee b/app/assets/javascripts/profile/profile_bundle.js.coffee deleted file mode 100644 index 91cacfece46..00000000000 --- a/app/assets/javascripts/profile/profile_bundle.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -# -#= require_tree . diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js new file mode 100644 index 00000000000..e6663177161 --- /dev/null +++ b/app/assets/javascripts/project.js @@ -0,0 +1,103 @@ +(function() { + this.Project = (function() { + function Project() { + $('ul.clone-options-dropdown a').click(function() { + var url; + if ($(this).hasClass('active')) { + return; + } + $('.active').not($(this)).removeClass('active'); + $(this).toggleClass('active'); + url = $("#project_clone").val(); + $('#project_clone').val(url); + return $('.clone').text(url); + }); + this.initRefSwitcher(); + $('.project-refs-select').on('change', function() { + return $(this).parents('form').submit(); + }); + $('.hide-no-ssh-message').on('click', function(e) { + var path; + path = '/'; + $.cookie('hide_no_ssh_message', 'false', { + path: path + }); + $(this).parents('.no-ssh-key-message').remove(); + return e.preventDefault(); + }); + $('.hide-no-password-message').on('click', function(e) { + var path; + path = '/'; + $.cookie('hide_no_password_message', 'false', { + path: path + }); + $(this).parents('.no-password-message').remove(); + return e.preventDefault(); + }); + this.projectSelectDropdown(); + } + + Project.prototype.projectSelectDropdown = function() { + new ProjectSelect(); + $('.project-item-select').on('click', (function(_this) { + return function(e) { + return _this.changeProject($(e.currentTarget).val()); + }; + })(this)); + return $('.js-projects-dropdown-toggle').on('click', function(e) { + e.preventDefault(); + return $('.js-projects-dropdown').select2('open'); + }); + }; + + Project.prototype.changeProject = function(url) { + return window.location = url; + }; + + Project.prototype.initRefSwitcher = function() { + return $('.js-project-refs-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref') + } + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterByText: true, + fieldName: 'ref', + renderRow: function(ref) { + var link; + if (ref.header != null) { + return $('<li />').addClass('dropdown-header').text(ref.header); + } else { + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('<li />').append(link); + } + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(e) { + return $dropdown.closest('form').submit(); + } + }); + }); + }; + + return Project; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee deleted file mode 100644 index 3288c801388..00000000000 --- a/app/assets/javascripts/project.js.coffee +++ /dev/null @@ -1,91 +0,0 @@ -class @Project - constructor: -> - # Git protocol switcher - $('ul.clone-options-dropdown a').click -> - return if $(@).hasClass('active') - - - # Remove the active class for all buttons (ssh, http, kerberos if shown) - $('.active').not($(@)).removeClass('active'); - # Add the active class for the clicked button - $(@).toggleClass('active') - - url = $("#project_clone").val() - - # Update the input field - $('#project_clone').val(url) - - # Update the command line instructions - $('.clone').text(url) - - # Ref switcher - @initRefSwitcher() - $('.project-refs-select').on 'change', -> - $(@).parents('form').submit() - - $('.hide-no-ssh-message').on 'click', (e) -> - path = '/' - $.cookie('hide_no_ssh_message', 'false', { path: path }) - $(@).parents('.no-ssh-key-message').remove() - e.preventDefault() - - $('.hide-no-password-message').on 'click', (e) -> - path = '/' - $.cookie('hide_no_password_message', 'false', { path: path }) - $(@).parents('.no-password-message').remove() - e.preventDefault() - - @projectSelectDropdown() - - projectSelectDropdown: -> - new ProjectSelect() - - $('.project-item-select').on 'click', (e) => - @changeProject $(e.currentTarget).val() - - $('.js-projects-dropdown-toggle').on 'click', (e) -> - e.preventDefault() - - $('.js-projects-dropdown').select2('open') - - changeProject: (url) -> - window.location = url - - initRefSwitcher: -> - $('.js-project-refs-dropdown').each -> - $dropdown = $(@) - selected = $dropdown.data('selected') - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: $dropdown.data('refs-url') - data: - ref: $dropdown.data('ref') - ).done (refs) -> - callback(refs) - selectable: true - filterable: true - filterByText: true - fieldName: 'ref' - renderRow: (ref) -> - if ref.header? - $('<li />') - .addClass('dropdown-header') - .text(ref.header) - else - link = $('<a />') - .attr('href', '#') - .addClass(if ref is selected then 'is-active' else '') - .text(ref) - .attr('data-ref', escape(ref)) - - $('<li />') - .append(link) - id: (obj, $el) -> - $el.attr('data-ref') - toggleLabel: (obj, $el) -> - $el.text().trim() - clicked: (e) -> - $dropdown.closest('form').submit() - ) diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js new file mode 100644 index 00000000000..277e71523d5 --- /dev/null +++ b/app/assets/javascripts/project_avatar.js @@ -0,0 +1,21 @@ +(function() { + this.ProjectAvatar = (function() { + function ProjectAvatar() { + $('.js-choose-project-avatar-button').bind('click', function() { + var form; + form = $(this).closest('form'); + return form.find('.js-project-avatar-input').click(); + }); + $('.js-project-avatar-input').bind('change', function() { + var filename, form; + form = $(this).closest('form'); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-avatar-filename').text(filename); + }); + } + + return ProjectAvatar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_avatar.js.coffee b/app/assets/javascripts/project_avatar.js.coffee deleted file mode 100644 index 8bec6e2ccca..00000000000 --- a/app/assets/javascripts/project_avatar.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -class @ProjectAvatar - constructor: -> - $('.js-choose-project-avatar-button').bind 'click', -> - form = $(this).closest('form') - form.find('.js-project-avatar-input').click() - $('.js-project-avatar-input').bind 'change', -> - form = $(this).closest('form') - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find('.js-avatar-filename').text(filename) diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js new file mode 100644 index 00000000000..4925f0519f0 --- /dev/null +++ b/app/assets/javascripts/project_find_file.js @@ -0,0 +1,170 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.ProjectFindFile = (function() { + var highlighter; + + function ProjectFindFile(element1, options) { + this.element = element1; + this.options = options; + this.goToBlob = bind(this.goToBlob, this); + this.goToTree = bind(this.goToTree, this); + this.selectRowDown = bind(this.selectRowDown, this); + this.selectRowUp = bind(this.selectRowUp, this); + this.filePaths = {}; + this.inputElement = this.element.find(".file-finder-input"); + this.initEvent(); + this.inputElement.focus(); + this.load(this.options.url); + } + + ProjectFindFile.prototype.initEvent = function() { + this.inputElement.off("keyup"); + this.inputElement.on("keyup", (function(_this) { + return function(event) { + var oldValue, ref, target, value; + target = $(event.target); + value = target.val(); + oldValue = (ref = target.data("oldValue")) != null ? ref : ""; + if (value !== oldValue) { + target.data("oldValue", value); + _this.findFile(); + return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); + } + }; + })(this)); + return this.element.find(".tree-content-holder .tree-table").on("click", function(event) { + var path; + if (event.target.nodeName !== "A") { + path = this.element.find(".tree-item-file-name a", this).attr("href"); + if (path) { + return location.href = path; + } + } + }); + }; + + ProjectFindFile.prototype.findFile = function() { + var result, searchText; + searchText = this.inputElement.val(); + result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; + return this.renderList(result, searchText); + }; + + ProjectFindFile.prototype.load = function(url) { + return $.ajax({ + url: url, + method: "get", + dataType: "json", + success: (function(_this) { + return function(data) { + _this.element.find(".loading").hide(); + _this.filePaths = data; + _this.findFile(); + return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); + }; + })(this) + }); + }; + + ProjectFindFile.prototype.renderList = function(filePaths, searchText) { + var blobItemUrl, filePath, html, i, j, len, matches, results; + this.element.find(".tree-table > tbody").empty(); + results = []; + for (i = j = 0, len = filePaths.length; j < len; i = ++j) { + filePath = filePaths[i]; + if (i === 20) { + break; + } + if (searchText) { + matches = fuzzaldrinPlus.match(filePath, searchText); + } + blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; + html = this.makeHtml(filePath, matches, blobItemUrl); + results.push(this.element.find(".tree-table > tbody").append(html)); + } + return results; + }; + + highlighter = function(element, text, matches) { + var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; + lastIndex = 0; + highlightText = ""; + matchedChars = []; + for (j = 0, len = matches.length; j < len; j++) { + matchIndex = matches[j]; + unmatched = text.substring(lastIndex, matchIndex); + if (unmatched) { + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + matchedChars = []; + element.append(document.createTextNode(unmatched)); + } + matchedChars.push(text[matchIndex]); + lastIndex = matchIndex + 1; + } + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + return element.append(document.createTextNode(text.substring(lastIndex))); + }; + + 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>"); + if (matches) { + $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); + } else { + $tr.find("a").attr("href", blobItemUrl).text(filePath); + } + return $tr; + }; + + ProjectFindFile.prototype.selectRow = function(type) { + var next, rows, selectedRow; + rows = this.element.find(".files-slider tr.tree-item"); + selectedRow = this.element.find(".files-slider tr.tree-item.selected"); + if (rows && rows.length > 0) { + if (selectedRow && selectedRow.length > 0) { + if (type === "UP") { + next = selectedRow.prev(); + } else if (type === "DOWN") { + next = selectedRow.next(); + } + if (next.length > 0) { + selectedRow.removeClass("selected"); + selectedRow = next; + } + } else { + selectedRow = rows.eq(0); + } + return selectedRow.addClass("selected").focus(); + } + }; + + ProjectFindFile.prototype.selectRowUp = function() { + return this.selectRow("UP"); + }; + + ProjectFindFile.prototype.selectRowDown = function() { + return this.selectRow("DOWN"); + }; + + ProjectFindFile.prototype.goToTree = function() { + return location.href = this.options.treeUrl; + }; + + ProjectFindFile.prototype.goToBlob = function() { + var path; + path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href"); + if (path) { + return location.href = path; + } + }; + + return ProjectFindFile; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee deleted file mode 100644 index 0dd32352c34..00000000000 --- a/app/assets/javascripts/project_find_file.js.coffee +++ /dev/null @@ -1,125 +0,0 @@ -class @ProjectFindFile
- constructor: (@element, @options)->
- @filePaths = {}
- @inputElement = @element.find(".file-finder-input")
-
- # init event
- @initEvent()
-
- # focus text input box
- @inputElement.focus()
-
- # load file list
- @load(@options.url)
-
- # init event
- initEvent: ->
- @inputElement.off "keyup"
- @inputElement.on "keyup", (event) =>
- target = $(event.target)
- value = target.val()
- oldValue = target.data("oldValue") ? ""
-
- if value != oldValue
- target.data("oldValue", value)
- @findFile()
- @element.find("tr.tree-item").eq(0).addClass("selected").focus()
-
- @element.find(".tree-content-holder .tree-table").on "click", (event) ->
- if (event.target.nodeName != "A")
- path = @element.find(".tree-item-file-name a", this).attr("href")
- location.href = path if path
-
- # find file
- findFile: ->
- searchText = @inputElement.val()
- result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
- @renderList result, searchText
-
- # files pathes load
- load: (url) ->
- $.ajax
- url: url
- method: "get"
- dataType: "json"
- success: (data) =>
- @element.find(".loading").hide()
- @filePaths = data
- @findFile()
- @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
-
- # render result
- renderList: (filePaths, searchText) ->
- @element.find(".tree-table > tbody").empty()
-
- for filePath, i in filePaths
- break if i == 20
-
- if searchText
- matches = fuzzaldrinPlus.match(filePath, searchText)
-
- blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
-
- html = @makeHtml filePath, matches, blobItemUrl
- @element.find(".tree-table > tbody").append(html)
-
- # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
- highlighter = (element, text, matches) ->
- lastIndex = 0
- highlightText = ""
- matchedChars = []
-
- for matchIndex in matches
- unmatched = text.substring(lastIndex, matchIndex)
-
- if unmatched
- element.append(matchedChars.join("").bold()) if matchedChars.length
- matchedChars = []
- element.append(document.createTextNode(unmatched))
-
- matchedChars.push(text[matchIndex])
- lastIndex = matchIndex + 1
-
- element.append(matchedChars.join("").bold()) if matchedChars.length
- element.append(document.createTextNode(text.substring(lastIndex)))
-
- # make tbody row html
- makeHtml: (filePath, matches, blobItemUrl) ->
- $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>")
- if matches
- $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
- else
- $tr.find("a").attr("href", blobItemUrl).text(filePath)
-
- return $tr
-
- selectRow: (type) ->
- rows = @element.find(".files-slider tr.tree-item")
- selectedRow = @element.find(".files-slider tr.tree-item.selected")
-
- if rows && rows.length > 0
- if selectedRow && selectedRow.length > 0
- if type == "UP"
- next = selectedRow.prev()
- else if type == "DOWN"
- next = selectedRow.next()
-
- if next.length > 0
- selectedRow.removeClass "selected"
- selectedRow = next
- else
- selectedRow = rows.eq(0)
- selectedRow.addClass("selected").focus()
-
- selectRowUp: =>
- @selectRow "UP"
-
- selectRowDown: =>
- @selectRow "DOWN"
-
- goToTree: =>
- location.href = @options.treeUrl
-
- goToBlob: =>
- path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
- location.href = path if path
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js new file mode 100644 index 00000000000..d2261c51f35 --- /dev/null +++ b/app/assets/javascripts/project_fork.js @@ -0,0 +1,14 @@ +(function() { + this.ProjectFork = (function() { + function ProjectFork() { + $('.fork-thumbnail a').on('click', function() { + $('.fork-namespaces').hide(); + return $('.save-project-loader').show(); + }); + } + + return ProjectFork; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_fork.js.coffee b/app/assets/javascripts/project_fork.js.coffee deleted file mode 100644 index e15a1c4ef76..00000000000 --- a/app/assets/javascripts/project_fork.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class @ProjectFork - constructor: -> - $('.fork-thumbnail a').on 'click', -> - $('.fork-namespaces').hide() - $('.save-project-loader').show() diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js new file mode 100644 index 00000000000..c61b0cf2fde --- /dev/null +++ b/app/assets/javascripts/project_import.js @@ -0,0 +1,13 @@ +(function() { + this.ProjectImport = (function() { + function ProjectImport() { + setTimeout(function() { + return Turbolinks.visit(location.href); + }, 5000); + } + + return ProjectImport; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_import.js.coffee b/app/assets/javascripts/project_import.js.coffee deleted file mode 100644 index 6633564a079..00000000000 --- a/app/assets/javascripts/project_import.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class @ProjectImport - constructor: -> - setTimeout -> - Turbolinks.visit(location.href) - , 5000 diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js new file mode 100644 index 00000000000..f6a796b325a --- /dev/null +++ b/app/assets/javascripts/project_members.js @@ -0,0 +1,13 @@ +(function() { + this.ProjectMembers = (function() { + function ProjectMembers() { + $('li.project_member').bind('ajax:success', function() { + return $(this).fadeOut(); + }); + } + + return ProjectMembers; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_members.js.coffee b/app/assets/javascripts/project_members.js.coffee deleted file mode 100644 index 896ba7e53ee..00000000000 --- a/app/assets/javascripts/project_members.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @ProjectMembers - constructor: -> - $('li.project_member').bind 'ajax:success', -> - $(this).fadeOut() diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js new file mode 100644 index 00000000000..798f15e40a0 --- /dev/null +++ b/app/assets/javascripts/project_new.js @@ -0,0 +1,40 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.ProjectNew = (function() { + function ProjectNew() { + this.toggleSettings = bind(this.toggleSettings, this); + $('.project-edit-container').on('ajax:before', (function(_this) { + return function() { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }; + })(this)); + this.toggleSettings(); + this.toggleSettingsOnclick(); + } + + ProjectNew.prototype.toggleSettings = function() { + this._showOrHide('#project_builds_enabled', '.builds-feature'); + return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature'); + }; + + ProjectNew.prototype.toggleSettingsOnclick = function() { + return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings); + }; + + ProjectNew.prototype._showOrHide = function(checkElement, container) { + var $container; + $container = $(container); + if ($(checkElement).prop('checked')) { + return $container.show(); + } else { + return $container.hide(); + } + }; + + return ProjectNew; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee deleted file mode 100644 index e48343a19b5..00000000000 --- a/app/assets/javascripts/project_new.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -class @ProjectNew - constructor: -> - $('.project-edit-container').on 'ajax:before', => - $('.project-edit-container').hide() - $('.save-project-loader').show() - @toggleSettings() - @toggleSettingsOnclick() - - - toggleSettings: => - @_showOrHide('#project_builds_enabled', '.builds-feature') - @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature') - - toggleSettingsOnclick: -> - $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings - - _showOrHide: (checkElement, container) -> - $container = $(container) - - if $(checkElement).prop('checked') - $container.show() - else - $container.hide() diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js new file mode 100644 index 00000000000..20b147500cf --- /dev/null +++ b/app/assets/javascripts/project_select.js @@ -0,0 +1,102 @@ +(function() { + this.ProjectSelect = (function() { + function ProjectSelect() { + $('.js-projects-dropdown-toggle').each(function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return $dropdown.glDropdown({ + filterable: true, + filterRemote: true, + search: { + fields: ['name_with_namespace'] + }, + data: function(term, callback) { + var finalCallback, projectsCallback; + finalCallback = function(projects) { + return callback(projects); + }; + if (this.includeGroups) { + projectsCallback = function(projects) { + var groupsCallback; + groupsCallback = function(groups) { + var data; + data = groups.concat(projects); + return finalCallback(data); + }; + return Api.groups(term, false, groupsCallback); + }; + } else { + projectsCallback = finalCallback; + } + if (this.groupId) { + return Api.groupProjects(this.groupId, term, projectsCallback); + } else { + return Api.projects(term, this.orderBy, projectsCallback); + } + }, + url: function(project) { + return project.web_url; + }, + text: function(project) { + return project.name_with_namespace; + } + }); + }); + $('.ajax-project-select').each(function(i, select) { + var placeholder; + this.groupId = $(select).data('group-id'); + this.includeGroups = $(select).data('include-groups'); + this.orderBy = $(select).data('order-by') || 'id'; + placeholder = "Search for project"; + if (this.includeGroups) { + placeholder += " or group"; + } + return $(select).select2({ + placeholder: placeholder, + minimumInputLength: 0, + query: (function(_this) { + return function(query) { + var finalCallback, projectsCallback; + finalCallback = function(projects) { + var data; + data = { + results: projects + }; + return query.callback(data); + }; + if (_this.includeGroups) { + projectsCallback = function(projects) { + var groupsCallback; + groupsCallback = function(groups) { + var data; + data = groups.concat(projects); + return finalCallback(data); + }; + return Api.groups(query.term, false, groupsCallback); + }; + } else { + projectsCallback = finalCallback; + } + if (_this.groupId) { + return Api.groupProjects(_this.groupId, query.term, projectsCallback); + } else { + return Api.projects(query.term, _this.orderBy, projectsCallback); + } + }; + })(this), + id: function(project) { + return project.web_url; + }, + text: function(project) { + return project.name_with_namespace || project.name; + }, + dropdownCssClass: "ajax-project-dropdown" + }); + }); + } + + return ProjectSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee deleted file mode 100644 index 704bd8dee53..00000000000 --- a/app/assets/javascripts/project_select.js.coffee +++ /dev/null @@ -1,72 +0,0 @@ -class @ProjectSelect - constructor: -> - $('.js-projects-dropdown-toggle').each (i, dropdown) -> - $dropdown = $(dropdown) - - $dropdown.glDropdown( - filterable: true - filterRemote: true - search: - fields: ['name_with_namespace'] - data: (term, callback) -> - finalCallback = (projects) -> - callback projects - - if @includeGroups - projectsCallback = (projects) -> - groupsCallback = (groups) -> - data = groups.concat(projects) - finalCallback(data) - - Api.groups term, false, groupsCallback - else - projectsCallback = finalCallback - - if @groupId - Api.groupProjects @groupId, term, projectsCallback - else - Api.projects term, @orderBy, projectsCallback - url: (project) -> - project.web_url - text: (project) -> - project.name_with_namespace - ) - - $('.ajax-project-select').each (i, select) -> - @groupId = $(select).data('group-id') - @includeGroups = $(select).data('include-groups') - @orderBy = $(select).data('order-by') || 'id' - - placeholder = "Search for project" - placeholder += " or group" if @includeGroups - - $(select).select2 - placeholder: placeholder - minimumInputLength: 0 - query: (query) => - finalCallback = (projects) -> - data = { results: projects } - query.callback(data) - - if @includeGroups - projectsCallback = (projects) -> - groupsCallback = (groups) -> - data = groups.concat(projects) - finalCallback(data) - - Api.groups query.term, false, groupsCallback - else - projectsCallback = finalCallback - - if @groupId - Api.groupProjects @groupId, query.term, projectsCallback - else - Api.projects query.term, @orderBy, projectsCallback - - id: (project) -> - project.web_url - - text: (project) -> - project.name_with_namespace || project.name - - dropdownCssClass: "ajax-project-dropdown" diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js new file mode 100644 index 00000000000..8ca4c427912 --- /dev/null +++ b/app/assets/javascripts/project_show.js @@ -0,0 +1,9 @@ +(function() { + this.ProjectShow = (function() { + function ProjectShow() {} + + return ProjectShow; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee deleted file mode 100644 index 1fdf28f2528..00000000000 --- a/app/assets/javascripts/project_show.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -class @ProjectShow - constructor: -> - # I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js new file mode 100644 index 00000000000..4f415b05dbc --- /dev/null +++ b/app/assets/javascripts/projects_list.js @@ -0,0 +1,48 @@ +(function() { + this.ProjectsList = { + init: function() { + $(".projects-list-filter").off('keyup'); + this.initSearch(); + return this.initPagination(); + }, + initSearch: function() { + var debounceFilter, projectsListFilter; + projectsListFilter = $('.projects-list-filter'); + debounceFilter = _.debounce(ProjectsList.filterResults, 500); + return projectsListFilter.on('keyup', function(e) { + if (projectsListFilter.val() !== '') { + return debounceFilter(); + } + }); + }, + filterResults: function() { + var form, project_filter_url, search; + $('.projects-list-holder').fadeTo(250, 0.5); + form = null; + form = $("form#project-filter-form"); + search = $(".projects-list-filter").val(); + project_filter_url = form.attr('action') + '?' + form.serialize(); + return $.ajax({ + type: "GET", + url: form.attr('action'), + data: form.serialize(), + complete: function() { + return $('.projects-list-holder').fadeTo(250, 1); + }, + success: function(data) { + $('.projects-list-holder').replaceWith(data.html); + return history.replaceState({ + page: project_filter_url + }, document.title, project_filter_url); + }, + dataType: "json" + }); + }, + initPagination: function() { + return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) { + return $('.projects-list-holder').replaceWith(data.html); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee deleted file mode 100644 index a7d78d9e461..00000000000 --- a/app/assets/javascripts/projects_list.js.coffee +++ /dev/null @@ -1,36 +0,0 @@ -@ProjectsList = - init: -> - $(".projects-list-filter").off('keyup') - this.initSearch() - this.initPagination() - - initSearch: -> - projectsListFilter = $('.projects-list-filter') - debounceFilter = _.debounce ProjectsList.filterResults, 500 - projectsListFilter.on 'keyup', (e) -> - debounceFilter() if projectsListFilter.val() isnt '' - - filterResults: -> - $('.projects-list-holder').fadeTo(250, 0.5) - - form = null - form = $("form#project-filter-form") - search = $(".projects-list-filter").val() - project_filter_url = form.attr('action') + '?' + form.serialize() - - $.ajax - type: "GET" - url: form.attr('action') - data: form.serialize() - complete: -> - $('.projects-list-holder').fadeTo(250, 1) - success: (data) -> - $('.projects-list-holder').replaceWith(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: project_filter_url}, document.title, project_filter_url - dataType: "json" - - initPagination: -> - $('.projects-list-holder .pagination').on('ajax:success', (e, data) -> - $('.projects-list-holder').replaceWith(data.html) - ) diff --git a/app/assets/javascripts/protected_branch_select.js b/app/assets/javascripts/protected_branch_select.js new file mode 100644 index 00000000000..3a47fc972dc --- /dev/null +++ b/app/assets/javascripts/protected_branch_select.js @@ -0,0 +1,72 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.ProtectedBranchSelect = (function() { + function ProtectedBranchSelect(currentProject) { + this.toggleCreateNewButton = bind(this.toggleCreateNewButton, this); + this.getProtectedBranches = bind(this.getProtectedBranches, this); + $('.dropdown-footer').hide(); + this.dropdown = $('.js-protected-branch-select').glDropdown({ + data: this.getProtectedBranches, + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel: function(selected) { + if (selected && 'id' in selected) { + return selected.title; + } else { + return 'Protected Branch'; + } + }, + fieldName: 'protected_branch[name]', + text: function(protected_branch) { + return _.escape(protected_branch.title); + }, + id: function(protected_branch) { + return _.escape(protected_branch.id); + }, + onFilter: this.toggleCreateNewButton, + clicked: function() { + return $('.protect-branch-btn').attr('disabled', false); + } + }); + $('.create-new-protected-branch').on('click', (function(_this) { + return function(event) { + _this.dropdown.data('glDropdown').remote.execute(); + return _this.dropdown.data('glDropdown').selectRowAtIndex(event, 0); + }; + })(this)); + } + + ProtectedBranchSelect.prototype.getProtectedBranches = function(term, callback) { + if (this.selectedBranch) { + return callback(gon.open_branches.concat(this.selectedBranch)); + } else { + return callback(gon.open_branches); + } + }; + + ProtectedBranchSelect.prototype.toggleCreateNewButton = function(branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName + }; + if (branchName === '') { + $('.protected-branch-select-footer-list').addClass('hidden'); + return $('.dropdown-footer').hide(); + } else { + $('.create-new-protected-branch').text("Create Protected Branch: " + branchName); + $('.protected-branch-select-footer-list').removeClass('hidden'); + return $('.dropdown-footer').show(); + } + }; + + return ProtectedBranchSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/protected_branch_select.js.coffee b/app/assets/javascripts/protected_branch_select.js.coffee deleted file mode 100644 index 6d45770ace9..00000000000 --- a/app/assets/javascripts/protected_branch_select.js.coffee +++ /dev/null @@ -1,40 +0,0 @@ -class @ProtectedBranchSelect - constructor: (currentProject) -> - $('.dropdown-footer').hide(); - @dropdown = $('.js-protected-branch-select').glDropdown( - data: @getProtectedBranches - filterable: true - remote: false - search: - fields: ['title'] - selectable: true - toggleLabel: (selected) -> if (selected and 'id' of selected) then selected.title else 'Protected Branch' - fieldName: 'protected_branch[name]' - text: (protected_branch) -> _.escape(protected_branch.title) - id: (protected_branch) -> _.escape(protected_branch.id) - onFilter: @toggleCreateNewButton - clicked: () -> $('.protect-branch-btn').attr('disabled', false) - ) - - $('.create-new-protected-branch').on 'click', (event) => - # Refresh the dropdown's data, which ends up calling `getProtectedBranches` - @dropdown.data('glDropdown').remote.execute() - @dropdown.data('glDropdown').selectRowAtIndex(event, 0) - - getProtectedBranches: (term, callback) => - if @selectedBranch - callback(gon.open_branches.concat(@selectedBranch)) - else - callback(gon.open_branches) - - toggleCreateNewButton: (branchName) => - @selectedBranch = { title: branchName, id: branchName, text: branchName } - - if branchName is '' - $('.protected-branch-select-footer-list').addClass('hidden') - $('.dropdown-footer').hide(); - else - $('.create-new-protected-branch').text("Create Protected Branch: #{branchName}") - $('.protected-branch-select-footer-list').removeClass('hidden') - $('.dropdown-footer').show(); - diff --git a/app/assets/javascripts/protected_branches.js b/app/assets/javascripts/protected_branches.js new file mode 100644 index 00000000000..db21a19964d --- /dev/null +++ b/app/assets/javascripts/protected_branches.js @@ -0,0 +1,35 @@ +(function() { + $(function() { + return $(".protected-branches-list :checkbox").change(function(e) { + var can_push, id, name, obj, url; + name = $(this).attr("name"); + if (name === "developers_can_push" || name === "developers_can_merge") { + id = $(this).val(); + can_push = $(this).is(":checked"); + url = $(this).data("url"); + return $.ajax({ + type: "PATCH", + url: url, + dataType: "json", + data: { + id: id, + protected_branch: ( + obj = {}, + obj["" + name] = can_push, + obj + ) + }, + success: function() { + var row; + row = $(e.target); + return row.closest('tr').effect('highlight'); + }, + error: function() { + return new Flash("Failed to update branch!", "alert"); + } + }); + } + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee deleted file mode 100644 index 14afef2e2ee..00000000000 --- a/app/assets/javascripts/protected_branches.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -$ -> - $(".protected-branches-list :checkbox").change (e) -> - name = $(this).attr("name") - if name == "developers_can_push" || name == "developers_can_merge" - id = $(this).val() - can_push = $(this).is(":checked") - url = $(this).data("url") - $.ajax - type: "PATCH" - url: url - dataType: "json" - data: - id: id - protected_branch: - "#{name}": can_push - - success: -> - row = $(e.target) - row.closest('tr').effect('highlight') - - error: -> - new Flash("Failed to update branch!", "alert") diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js new file mode 100644 index 00000000000..dc4d5113826 --- /dev/null +++ b/app/assets/javascripts/right_sidebar.js @@ -0,0 +1,201 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Sidebar = (function() { + function Sidebar(currentUser) { + this.toggleTodo = bind(this.toggleTodo, this); + this.sidebar = $('aside'); + this.addEventListeners(); + } + + Sidebar.prototype.addEventListeners = function() { + this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); + $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); + $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); + $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); + $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) { + var $allGutterToggleIcons, $this, $thisIcon; + e.preventDefault(); + $this = $(this); + $thisIcon = $this.find('i'); + $allGutterToggleIcons = $('.js-sidebar-toggle i'); + if ($thisIcon.hasClass('fa-angle-double-right')) { + $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); + $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + } else { + $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); + $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + } + if (!triggered) { + return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), { + path: '/' + }); + } + }); + return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); + }; + + Sidebar.prototype.toggleTodo = function(e) { + var $btnText, $this, $todoLoading, ajaxType, url; + $this = $(e.currentTarget); + $todoLoading = $('.js-issuable-todo-loading'); + $btnText = $('.js-issuable-todo-text', $this); + ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; + if ($this.attr('data-delete-path')) { + url = "" + ($this.attr('data-delete-path')); + } else { + url = "" + ($this.data('url')); + } + return $.ajax({ + url: url, + type: ajaxType, + dataType: 'json', + data: { + issuable_id: $this.data('issuable-id'), + issuable_type: $this.data('issuable-type') + }, + beforeSend: (function(_this) { + return function() { + return _this.beforeTodoSend($this, $todoLoading); + }; + })(this) + }).done((function(_this) { + return function(data) { + return _this.todoUpdateDone(data, $this, $btnText, $todoLoading); + }; + })(this)); + }; + + Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) { + $btn.disable(); + return $todoLoading.removeClass('hidden'); + }; + + Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) { + var $todoPendingCount; + $todoPendingCount = $('.todos-pending-count'); + $todoPendingCount.text(data.count); + $btn.enable(); + $todoLoading.addClass('hidden'); + if (data.count === 0) { + $todoPendingCount.addClass('hidden'); + } else { + $todoPendingCount.removeClass('hidden'); + } + if (data.delete_path != null) { + $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path); + return $btnText.text($btn.data('mark-text')); + } else { + $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path'); + return $btnText.text($btn.data('todo-text')); + } + }; + + Sidebar.prototype.sidebarDropdownLoading = function(e) { + var $loading, $sidebarCollapsedIcon, i, img; + $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + img = $sidebarCollapsedIcon.find('img'); + i = $sidebarCollapsedIcon.find('i'); + $loading = $('<i class="fa fa-spinner fa-spin"></i>'); + if (img.length) { + img.before($loading); + return img.hide(); + } else if (i.length) { + i.before($loading); + return i.hide(); + } + }; + + Sidebar.prototype.sidebarDropdownLoaded = function(e) { + var $sidebarCollapsedIcon, i, img; + $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + img = $sidebarCollapsedIcon.find('img'); + $sidebarCollapsedIcon.find('i.fa-spin').remove(); + i = $sidebarCollapsedIcon.find('i'); + if (img.length) { + return img.show(); + } else { + return i.show(); + } + }; + + Sidebar.prototype.sidebarCollapseClicked = function(e) { + var $block, sidebar; + if ($(e.currentTarget).hasClass('dont-change-state')) { + return; + } + sidebar = e.data; + e.preventDefault(); + $block = $(this).closest('.block'); + return sidebar.openDropdown($block); + }; + + Sidebar.prototype.openDropdown = function(blockOrName) { + var $block; + $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; + $block.find('.edit-link').trigger('click'); + if (!this.isOpen()) { + this.setCollapseAfterUpdate($block); + return this.toggleSidebar('open'); + } + }; + + Sidebar.prototype.setCollapseAfterUpdate = function($block) { + $block.addClass('collapse-after-update'); + return $('.page-with-sidebar').addClass('with-overlay'); + }; + + Sidebar.prototype.onSidebarDropdownHidden = function(e) { + var $block, sidebar; + sidebar = e.data; + e.preventDefault(); + $block = $(this).closest('.block'); + return sidebar.sidebarDropdownHidden($block); + }; + + Sidebar.prototype.sidebarDropdownHidden = function($block) { + if ($block.hasClass('collapse-after-update')) { + $block.removeClass('collapse-after-update'); + $('.page-with-sidebar').removeClass('with-overlay'); + return this.toggleSidebar('hide'); + } + }; + + Sidebar.prototype.triggerOpenSidebar = function() { + return this.sidebar.find('.js-sidebar-toggle').trigger('click'); + }; + + Sidebar.prototype.toggleSidebar = function(action) { + if (action == null) { + action = 'toggle'; + } + if (action === 'toggle') { + this.triggerOpenSidebar(); + } + if (action === 'open') { + if (!this.isOpen()) { + this.triggerOpenSidebar(); + } + } + if (action === 'hide') { + if (this.isOpen()) { + return this.triggerOpenSidebar(); + } + } + }; + + Sidebar.prototype.isOpen = function() { + return this.sidebar.is('.right-sidebar-expanded'); + }; + + Sidebar.prototype.getBlock = function(name) { + return this.sidebar.find(".block." + name); + }; + + return Sidebar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee deleted file mode 100644 index 12340bbce54..00000000000 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ /dev/null @@ -1,172 +0,0 @@ -class @Sidebar - constructor: (currentUser) -> - @sidebar = $('aside') - - @addEventListeners() - - addEventListeners: -> - @sidebar.on('click', '.sidebar-collapsed-icon', @, @sidebarCollapseClicked) - $('.dropdown').on('hidden.gl.dropdown', @, @onSidebarDropdownHidden) - $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) - $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) - - - $(document) - .off 'click', '.js-sidebar-toggle' - .on 'click', '.js-sidebar-toggle', (e, triggered) -> - e.preventDefault() - $this = $(this) - $thisIcon = $this.find 'i' - $allGutterToggleIcons = $('.js-sidebar-toggle i') - if $thisIcon.hasClass('fa-angle-double-right') - $allGutterToggleIcons - .removeClass('fa-angle-double-right') - .addClass('fa-angle-double-left') - $('aside.right-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - $('.page-with-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - else - $allGutterToggleIcons - .removeClass('fa-angle-double-left') - .addClass('fa-angle-double-right') - $('aside.right-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - $('.page-with-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - if not triggered - $.cookie("collapsed_gutter", - $('.right-sidebar') - .hasClass('right-sidebar-collapsed'), { path: '/' }) - - $(document) - .off 'click', '.js-issuable-todo' - .on 'click', '.js-issuable-todo', @toggleTodo - - toggleTodo: (e) => - $this = $(e.currentTarget) - $todoLoading = $('.js-issuable-todo-loading') - $btnText = $('.js-issuable-todo-text', $this) - ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST' - - if $this.attr('data-delete-path') - url = "#{$this.attr('data-delete-path')}" - else - url = "#{$this.data('url')}" - - $.ajax( - url: url - type: ajaxType - dataType: 'json' - data: - issuable_id: $this.data('issuable-id') - issuable_type: $this.data('issuable-type') - beforeSend: => - @beforeTodoSend($this, $todoLoading) - ).done (data) => - @todoUpdateDone(data, $this, $btnText, $todoLoading) - - beforeTodoSend: ($btn, $todoLoading) -> - $btn.disable() - $todoLoading.removeClass 'hidden' - - todoUpdateDone: (data, $btn, $btnText, $todoLoading) -> - $todoPendingCount = $('.todos-pending-count') - $todoPendingCount.text data.count - - $btn.enable() - $todoLoading.addClass 'hidden' - - if data.count is 0 - $todoPendingCount.addClass 'hidden' - else - $todoPendingCount.removeClass 'hidden' - - if data.delete_path? - $btn - .attr 'aria-label', $btn.data('mark-text') - .attr 'data-delete-path', data.delete_path - $btnText.text $btn.data('mark-text') - else - $btn - .attr 'aria-label', $btn.data('todo-text') - .removeAttr 'data-delete-path' - $btnText.text $btn.data('todo-text') - - sidebarDropdownLoading: (e) -> - $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') - img = $sidebarCollapsedIcon.find('img') - i = $sidebarCollapsedIcon.find('i') - $loading = $('<i class="fa fa-spinner fa-spin"></i>') - if img.length - img.before($loading) - img.hide() - else if i.length - i.before($loading) - i.hide() - - sidebarDropdownLoaded: (e) -> - $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') - img = $sidebarCollapsedIcon.find('img') - $sidebarCollapsedIcon.find('i.fa-spin').remove() - i = $sidebarCollapsedIcon.find('i') - if img.length - img.show() - else - i.show() - - sidebarCollapseClicked: (e) -> - sidebar = e.data - e.preventDefault() - $block = $(@).closest('.block') - sidebar.openDropdown($block); - - openDropdown: (blockOrName) -> - $block = if _.isString(blockOrName) then @getBlock(blockOrName) else blockOrName - - $block.find('.edit-link').trigger('click') - - if not @isOpen() - @setCollapseAfterUpdate($block) - @toggleSidebar('open') - - setCollapseAfterUpdate: ($block) -> - $block.addClass('collapse-after-update') - $('.page-with-sidebar').addClass('with-overlay') - - onSidebarDropdownHidden: (e) -> - sidebar = e.data - e.preventDefault() - $block = $(@).closest('.block') - sidebar.sidebarDropdownHidden($block) - - sidebarDropdownHidden: ($block) -> - if $block.hasClass('collapse-after-update') - $block.removeClass('collapse-after-update') - $('.page-with-sidebar').removeClass('with-overlay') - @toggleSidebar('hide') - - triggerOpenSidebar: -> - @sidebar - .find('.js-sidebar-toggle') - .trigger('click') - - toggleSidebar: (action = 'toggle') -> - if action is 'toggle' - @triggerOpenSidebar() - - if action is 'open' - @triggerOpenSidebar() if not @isOpen() - - if action is 'hide' - @triggerOpenSidebar() if @isOpen() - - isOpen: -> - @sidebar.is('.right-sidebar-expanded') - - getBlock: (name) -> - @sidebar.find(".block.#{name}") diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js new file mode 100644 index 00000000000..d34346f862b --- /dev/null +++ b/app/assets/javascripts/search.js @@ -0,0 +1,93 @@ +(function() { + this.Search = (function() { + function Search() { + var $groupDropdown, $projectDropdown; + $groupDropdown = $('.js-search-group-dropdown'); + $projectDropdown = $('.js-search-project-dropdown'); + this.eventListeners(); + $groupDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'group_id', + data: function(term, callback) { + return Api.groups(term, null, function(data) { + data.unshift({ + name: 'Any' + }); + data.splice(1, 0, 'divider'); + return callback(data); + }); + }, + id: function(obj) { + return obj.id; + }, + text: function(obj) { + return obj.name; + }, + toggleLabel: function(obj) { + return ($groupDropdown.data('default-label')) + " " + obj.name; + }, + clicked: (function(_this) { + return function() { + return _this.submitSearch(); + }; + })(this) + }); + $projectDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'project_id', + data: function(term, callback) { + return Api.projects(term, 'id', function(data) { + data.unshift({ + name_with_namespace: 'Any' + }); + data.splice(1, 0, 'divider'); + return callback(data); + }); + }, + id: function(obj) { + return obj.id; + }, + text: function(obj) { + return obj.name_with_namespace; + }, + toggleLabel: function(obj) { + return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace; + }, + clicked: (function(_this) { + return function() { + return _this.submitSearch(); + }; + })(this) + }); + } + + Search.prototype.eventListeners = function() { + $(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp); + return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField); + }; + + Search.prototype.submitSearch = function() { + return $('.js-search-form').submit(); + }; + + Search.prototype.searchKeyUp = function() { + var $input; + $input = $(this); + if ($input.val() === '') { + return $('.js-search-clear').addClass('hidden'); + } else { + return $('.js-search-clear').removeClass('hidden'); + } + }; + + Search.prototype.clearSearchField = function() { + return $('.js-search-input').val('').trigger('keyup').focus(); + }; + + return Search; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/search.js.coffee b/app/assets/javascripts/search.js.coffee deleted file mode 100644 index 661e1195f60..00000000000 --- a/app/assets/javascripts/search.js.coffee +++ /dev/null @@ -1,75 +0,0 @@ -class @Search - constructor: -> - $groupDropdown = $('.js-search-group-dropdown') - $projectDropdown = $('.js-search-project-dropdown') - @eventListeners() - - $groupDropdown.glDropdown( - selectable: true - filterable: true - fieldName: 'group_id' - data: (term, callback) -> - Api.groups term, null, (data) -> - data.unshift( - name: 'Any' - ) - data.splice 1, 0, 'divider' - - callback(data) - id: (obj) -> - obj.id - text: (obj) -> - obj.name - toggleLabel: (obj) -> - "#{$groupDropdown.data('default-label')} #{obj.name}" - clicked: => - @submitSearch() - ) - - $projectDropdown.glDropdown( - selectable: true - filterable: true - fieldName: 'project_id' - data: (term, callback) -> - Api.projects term, 'id', (data) -> - data.unshift( - name_with_namespace: 'Any' - ) - data.splice 1, 0, 'divider' - - callback(data) - id: (obj) -> - obj.id - text: (obj) -> - obj.name_with_namespace - toggleLabel: (obj) -> - "#{$projectDropdown.data('default-label')} #{obj.name_with_namespace}" - clicked: => - @submitSearch() - ) - - eventListeners: -> - $(document) - .off 'keyup', '.js-search-input' - .on 'keyup', '.js-search-input', @searchKeyUp - - $(document) - .off 'click', '.js-search-clear' - .on 'click', '.js-search-clear', @clearSearchField - - submitSearch: -> - $('.js-search-form').submit() - - searchKeyUp: -> - $input = $(@) - - if $input.val() is '' - $('.js-search-clear').addClass 'hidden' - else - $('.js-search-clear').removeClass 'hidden' - - clearSearchField: -> - $('.js-search-input') - .val '' - .trigger 'keyup' - .focus() diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js new file mode 100644 index 00000000000..990f6536eb2 --- /dev/null +++ b/app/assets/javascripts/search_autocomplete.js @@ -0,0 +1,360 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.SearchAutocomplete = (function() { + var KEYCODE; + + KEYCODE = { + ESCAPE: 27, + BACKSPACE: 8, + ENTER: 13 + }; + + function SearchAutocomplete(opts) { + var ref, ref1, ref2, ref3, ref4; + if (opts == null) { + opts = {}; + } + this.onSearchInputBlur = bind(this.onSearchInputBlur, this); + this.onClearInputClick = bind(this.onClearInputClick, this); + this.onSearchInputFocus = bind(this.onSearchInputFocus, this); + this.onSearchInputClick = bind(this.onSearchInputClick, this); + 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') || ''; + this.dropdown = this.wrap.find('.dropdown'); + this.dropdownContent = this.dropdown.find('.dropdown-content'); + this.locationBadgeEl = this.getElement('.location-badge'); + this.scopeInputEl = this.getElement('#scope'); + this.searchInput = this.getElement('.search-input'); + this.projectInputEl = this.getElement('#search_project_id'); + this.groupInputEl = this.getElement('#group_id'); + this.searchCodeInputEl = this.getElement('#search_code'); + this.repositoryInputEl = this.getElement('#repository_ref'); + this.clearInput = this.getElement('.js-clear-input'); + this.saveOriginalState(); + if (gon.current_user_id) { + this.createAutocomplete(); + } + this.searchInput.addClass('disabled'); + this.saveTextLength(); + this.bindEvents(); + } + + SearchAutocomplete.prototype.getElement = function(selector) { + return this.wrap.find(selector); + }; + + SearchAutocomplete.prototype.saveOriginalState = function() { + return this.originalState = this.serializeState(); + }; + + SearchAutocomplete.prototype.saveTextLength = function() { + return this.lastTextLength = this.searchInput.val().length; + }; + + SearchAutocomplete.prototype.createAutocomplete = function() { + return this.searchInput.glDropdown({ + filterInputBlur: false, + filterable: true, + filterRemote: true, + highlight: true, + enterCallback: false, + filterInput: 'input#search', + search: { + fields: ['text'] + }, + data: this.getData.bind(this), + selectable: true, + clicked: this.onClick.bind(this) + }); + }; + + SearchAutocomplete.prototype.getData = function(term, callback) { + var _this, contents, jqXHR; + _this = this; + if (!term) { + if (contents = this.getCategoryContents()) { + this.searchInput.data('glDropdown').filter.options.callback(contents); + this.enableAutocomplete(); + } + return; + } + if (this.loadingSuggestions) { + return; + } + this.loadingSuggestions = true; + return jqXHR = $.get(this.autocompletePath, { + project_id: this.projectId, + project_ref: this.projectRef, + term: term + }, function(response) { + var data, firstCategory, i, lastCategory, len, suggestion; + if (!response.length) { + _this.disableAutocomplete(); + return; + } + data = []; + firstCategory = true; + for (i = 0, len = response.length; i < len; i++) { + suggestion = response[i]; + if (lastCategory !== suggestion.category) { + if (!firstCategory) { + data.push('separator'); + } + if (firstCategory) { + firstCategory = false; + } + data.push({ + header: suggestion.category + }); + lastCategory = suggestion.category; + } + data.push({ + id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, + category: suggestion.category, + text: suggestion.label, + url: suggestion.url + }); + } + if (data.length) { + data.push('separator'); + data.push({ + text: "Result name contains \"" + term + "\"", + url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val()) + }); + } + return callback(data); + }).always(function() { + return _this.loadingSuggestions = false; + }); + }; + + SearchAutocomplete.prototype.getCategoryContents = function() { + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + userId = gon.current_user_id; + utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; + if (utils.isInGroupsPage() && groupOptions) { + options = groupOptions[utils.getGroupSlug()]; + } else if (utils.isInProjectPage() && projectOptions) { + options = projectOptions[utils.getProjectSlug()]; + } else if (dashboardOptions) { + options = dashboardOptions; + } + issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name; + items = [ + { + header: "" + name + }, { + text: 'Issues assigned to me', + url: issuesPath + "/?assignee_id=" + userId + }, { + text: "Issues I've created", + url: issuesPath + "/?author_id=" + userId + }, 'separator', { + text: 'Merge requests assigned to me', + url: mrPath + "/?assignee_id=" + userId + }, { + text: "Merge requests I've created", + url: mrPath + "/?author_id=" + userId + } + ]; + if (!name) { + items.splice(0, 1); + } + return items; + }; + + SearchAutocomplete.prototype.serializeState = function() { + return { + 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: this.locationBadgeEl.text() + }; + }; + + SearchAutocomplete.prototype.bindEvents = function() { + this.searchInput.on('keydown', this.onSearchInputKeyDown); + this.searchInput.on('keyup', this.onSearchInputKeyUp); + this.searchInput.on('click', this.onSearchInputClick); + this.searchInput.on('focus', this.onSearchInputFocus); + this.searchInput.on('blur', this.onSearchInputBlur); + this.clearInput.on('click', this.onClearInputClick); + return this.locationBadgeEl.on('click', (function(_this) { + return function() { + return _this.searchInput.focus(); + }; + })(this)); + }; + + SearchAutocomplete.prototype.enableAutocomplete = function() { + var _this; + if (!gon.current_user_id) { + return; + } + if (!this.dropdown.hasClass('open')) { + _this = this; + this.loadingSuggestions = false; + this.dropdown.addClass('open').trigger('shown.bs.dropdown'); + return this.searchInput.removeClass('disabled'); + } + }; + + SearchAutocomplete.prototype.onSearchInputKeyDown = function() { + return this.saveTextLength(); + }; + + SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { + switch (e.keyCode) { + case KEYCODE.BACKSPACE: + if (this.lastTextLength === 0 && this.badgePresent()) { + this.removeLocationBadge(); + } + if (this.lastTextLength === 1) { + this.disableAutocomplete(); + } + if (this.lastTextLength > 1) { + this.enableAutocomplete(); + } + break; + case KEYCODE.ESCAPE: + this.restoreOriginalState(); + break; + default: + if (this.searchInput.val() === '') { + this.disableAutocomplete(); + } else { + if (e.keyCode !== KEYCODE.ENTER) { + this.enableAutocomplete(); + } + } + } + this.wrap.toggleClass('has-value', !!e.target.value); + }; + + SearchAutocomplete.prototype.onSearchInputClick = function(e) { + return e.stopImmediatePropagation(); + }; + + SearchAutocomplete.prototype.onSearchInputFocus = function() { + this.isFocused = true; + this.wrap.addClass('search-active'); + if (this.getValue() === '') { + return this.getData(); + } + }; + + SearchAutocomplete.prototype.getValue = function() { + return this.searchInput.val(); + }; + + SearchAutocomplete.prototype.onClearInputClick = function(e) { + e.preventDefault(); + return this.searchInput.val('').focus(); + }; + + SearchAutocomplete.prototype.onSearchInputBlur = function(e) { + this.isFocused = false; + this.wrap.removeClass('search-active'); + if (this.searchInput.val() === '') { + return this.restoreOriginalState(); + } + }; + + SearchAutocomplete.prototype.addLocationBadge = function(item) { + var badgeText, category, value; + category = item.category != null ? item.category + ": " : ''; + value = item.value != null ? item.value : ''; + badgeText = "" + category + value; + this.locationBadgeEl.text(badgeText).show(); + return this.wrap.addClass('has-location-badge'); + }; + + SearchAutocomplete.prototype.hasLocationBadge = function() { + return this.wrap.is('.has-location-badge'); + }; + + SearchAutocomplete.prototype.restoreOriginalState = function() { + var i, input, inputs, len; + inputs = Object.keys(this.originalState); + for (i = 0, len = inputs.length; i < len; i++) { + input = inputs[i]; + this.getElement("#" + input).val(this.originalState[input]); + } + if (this.originalState._location === '') { + return this.locationBadgeEl.hide(); + } else { + return this.addLocationBadge({ + value: this.originalState._location + }); + } + }; + + SearchAutocomplete.prototype.badgePresent = function() { + return this.locationBadgeEl.length; + }; + + SearchAutocomplete.prototype.resetSearchState = function() { + var i, input, inputs, len, results; + inputs = Object.keys(this.originalState); + results = []; + for (i = 0, len = inputs.length; i < len; i++) { + input = inputs[i]; + if (input === '_location') { + break; + } + results.push(this.getElement("#" + input).val('')); + } + return results; + }; + + SearchAutocomplete.prototype.removeLocationBadge = function() { + this.locationBadgeEl.hide(); + this.resetSearchState(); + this.wrap.removeClass('has-location-badge'); + return this.disableAutocomplete(); + }; + + SearchAutocomplete.prototype.disableAutocomplete = function() { + this.searchInput.addClass('disabled'); + this.dropdown.removeClass('open'); + return this.restoreMenu(); + }; + + SearchAutocomplete.prototype.restoreMenu = function() { + var html; + html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; + return this.dropdownContent.html(html); + }; + + SearchAutocomplete.prototype.onClick = function(item, $el, e) { + if (location.pathname.indexOf(item.url) !== -1) { + e.preventDefault(); + if (!this.badgePresent) { + if (item.category === 'Projects') { + this.projectInputEl.val(item.id); + this.addLocationBadge({ + value: 'This project' + }); + } + if (item.category === 'Groups') { + this.groupInputEl.val(item.id); + this.addLocationBadge({ + value: 'This group' + }); + } + } + $el.removeClass('is-active'); + this.disableAutocomplete(); + return this.searchInput.val('').focus(); + } + }; + + return SearchAutocomplete; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee deleted file mode 100644 index 72b1d3dfb1e..00000000000 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ /dev/null @@ -1,334 +0,0 @@ -class @SearchAutocomplete - - KEYCODE = - ESCAPE: 27 - BACKSPACE: 8 - ENTER: 13 - - constructor: (opts = {}) -> - { - @wrap = $('.search') - - @optsEl = @wrap.find('.search-autocomplete-opts') - @autocompletePath = @optsEl.data('autocomplete-path') - @projectId = @optsEl.data('autocomplete-project-id') || '' - @projectRef = @optsEl.data('autocomplete-project-ref') ||Â '' - - } = opts - - # Dropdown Element - @dropdown = @wrap.find('.dropdown') - @dropdownContent = @dropdown.find('.dropdown-content') - - @locationBadgeEl = @getElement('.location-badge') - @scopeInputEl = @getElement('#scope') - @searchInput = @getElement('.search-input') - @projectInputEl = @getElement('#search_project_id') - @groupInputEl = @getElement('#group_id') - @searchCodeInputEl = @getElement('#search_code') - @repositoryInputEl = @getElement('#repository_ref') - @clearInput = @getElement('.js-clear-input') - - @saveOriginalState() - - # Only when user is logged in - @createAutocomplete() if gon.current_user_id - - @searchInput.addClass('disabled') - - @saveTextLength() - - @bindEvents() - - # Finds an element inside wrapper element - getElement: (selector) -> - @wrap.find(selector) - - saveOriginalState: -> - @originalState = @serializeState() - - saveTextLength: -> - @lastTextLength = @searchInput.val().length - - createAutocomplete: -> - @searchInput.glDropdown - filterInputBlur: false - filterable: true - filterRemote: true - highlight: true - enterCallback: false - filterInput: 'input#search' - search: - fields: ['text'] - data: @getData.bind(@) - selectable: true - clicked: @onClick.bind(@) - - getData: (term, callback) -> - _this = @ - - unless term - if contents = @getCategoryContents() - @searchInput.data('glDropdown').filter.options.callback contents - @enableAutocomplete() - - return - - # Prevent multiple ajax calls - return if @loadingSuggestions - - @loadingSuggestions = true - - jqXHR = $.get(@autocompletePath, { - project_id: @projectId - project_ref: @projectRef - term: term - }, (response) -> - # Hide dropdown menu if no suggestions returns - if !response.length - _this.disableAutocomplete() - return - - data = [] - - # List results - firstCategory = true - for suggestion in response - - # Add group header before list each group - if lastCategory isnt suggestion.category - data.push 'separator' if !firstCategory - - firstCategory = false if firstCategory - - data.push - header: suggestion.category - - lastCategory = suggestion.category - - data.push - id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}" - category: suggestion.category - text: suggestion.label - url: suggestion.url - - # Add option to proceed with the search - if data.length - data.push('separator') - data.push - text: "Result name contains \"#{term}\"" - url: "/search?\ - search=#{term}\ - &project_id=#{_this.projectInputEl.val()}\ - &group_id=#{_this.groupInputEl.val()}" - - callback(data) - ).always -> - _this.loadingSuggestions = false - - - getCategoryContents: -> - - userId = gon.current_user_id - { utils, projectOptions, groupOptions, dashboardOptions } = gl - - if utils.isInGroupsPage() and groupOptions - options = groupOptions[utils.getGroupSlug()] - - else if utils.isInProjectPage() and projectOptions - options = projectOptions[utils.getProjectSlug()] - - else if dashboardOptions - options = dashboardOptions - - { issuesPath, mrPath, name } = options - - items = [ - { header: "#{name}" } - { text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" } - { text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" } - 'separator' - { text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" } - { text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" } - ] - - items.splice 0, 1 unless name - - return items - - - serializeState: -> - { - # Search Criteria - search_project_id: @projectInputEl.val() - group_id: @groupInputEl.val() - search_code: @searchCodeInputEl.val() - repository_ref: @repositoryInputEl.val() - scope: @scopeInputEl.val() - - # Location badge - _location: @locationBadgeEl.text() - } - - bindEvents: -> - @searchInput.on 'keydown', @onSearchInputKeyDown - @searchInput.on 'keyup', @onSearchInputKeyUp - @searchInput.on 'click', @onSearchInputClick - @searchInput.on 'focus', @onSearchInputFocus - @searchInput.on 'blur', @onSearchInputBlur - @clearInput.on 'click', @onClearInputClick - @locationBadgeEl.on 'click', => - @searchInput.focus() - - enableAutocomplete: -> - # No need to enable anything if user is not logged in - return if !gon.current_user_id - - unless @dropdown.hasClass('open') - _this = @ - @loadingSuggestions = false - - @dropdown - .addClass('open') - .trigger('shown.bs.dropdown') - @searchInput.removeClass('disabled') - - onSearchInputKeyDown: => - # Saves last length of the entered text - @saveTextLength() - - onSearchInputKeyUp: (e) => - switch e.keyCode - when KEYCODE.BACKSPACE - # when trying to remove the location badge - if @lastTextLength is 0 and @badgePresent() - @removeLocationBadge() - - # When removing the last character and no badge is present - if @lastTextLength is 1 - @disableAutocomplete() - - # When removing any character from existin value - if @lastTextLength > 1 - @enableAutocomplete() - - when KEYCODE.ESCAPE - @restoreOriginalState() - - else - # Handle the case when deleting the input value other than backspace - # e.g. Pressing ctrl + backspace or ctrl + x - if @searchInput.val() is '' - @disableAutocomplete() - else - # We should display the menu only when input is not empty - @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER - - @wrap.toggleClass 'has-value', !!e.target.value - - # Avoid falsy value to be returned - return - - onSearchInputClick: (e) => - # Prevents closing the dropdown menu - e.stopImmediatePropagation() - - onSearchInputFocus: => - @isFocused = true - @wrap.addClass('search-active') - - @getData() if @getValue() is '' - - - getValue: -> return @searchInput.val() - - - onClearInputClick: (e) => - e.preventDefault() - @searchInput.val('').focus() - - onSearchInputBlur: (e) => - @isFocused = false - @wrap.removeClass('search-active') - - # If input is blank then restore state - if @searchInput.val() is '' - @restoreOriginalState() - - addLocationBadge: (item) -> - category = if item.category? then "#{item.category}: " else '' - value = if item.value? then item.value else '' - - badgeText = "#{category}#{value}" - @locationBadgeEl.text(badgeText).show() - @wrap.addClass('has-location-badge') - - - hasLocationBadge: -> return @wrap.is '.has-location-badge' - - - restoreOriginalState: -> - inputs = Object.keys @originalState - - for input in inputs - @getElement("##{input}").val(@originalState[input]) - - if @originalState._location is '' - @locationBadgeEl.hide() - else - @addLocationBadge( - value: @originalState._location - ) - - badgePresent: -> - @locationBadgeEl.length - - resetSearchState: -> - inputs = Object.keys @originalState - - for input in inputs - - # _location isnt a input - break if input is '_location' - - @getElement("##{input}").val('') - - - removeLocationBadge: -> - - @locationBadgeEl.hide() - @resetSearchState() - @wrap.removeClass('has-location-badge') - @disableAutocomplete() - - - disableAutocomplete: -> - @searchInput.addClass('disabled') - @dropdown.removeClass('open') - @restoreMenu() - - restoreMenu: -> - html = "<ul> - <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> - </ul>" - @dropdownContent.html(html) - - onClick: (item, $el, e) -> - if location.pathname.indexOf(item.url) isnt -1 - e.preventDefault() - if not @badgePresent - if item.category is 'Projects' - @projectInputEl.val(item.id) - @addLocationBadge( - value: 'This project' - ) - - if item.category is 'Groups' - @groupInputEl.val(item.id) - @addLocationBadge( - value: 'This group' - ) - - $el.removeClass('is-active') - @disableAutocomplete() - @searchInput.val('').focus() diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js new file mode 100644 index 00000000000..3b28332854a --- /dev/null +++ b/app/assets/javascripts/shortcuts.js @@ -0,0 +1,97 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Shortcuts = (function() { + function Shortcuts(skipResetBindings) { + this.onToggleHelp = bind(this.onToggleHelp, this); + this.enabledHelp = []; + if (!skipResetBindings) { + Mousetrap.reset(); + } + Mousetrap.bind('?', this.onToggleHelp); + Mousetrap.bind('s', Shortcuts.focusSearch); + Mousetrap.bind('f', (function(_this) { + return function(e) { + return _this.focusFilter(e); + }; + })(this)); + Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); + if (typeof findFileURL !== "undefined" && findFileURL !== null) { + Mousetrap.bind('t', function() { + return Turbolinks.visit(findFileURL); + }); + } + } + + Shortcuts.prototype.onToggleHelp = function(e) { + e.preventDefault(); + return Shortcuts.toggleHelp(this.enabledHelp); + }; + + Shortcuts.prototype.toggleMarkdownPreview = function(e) { + return $(document).triggerHandler('markdown-preview:toggle', [e]); + }; + + Shortcuts.toggleHelp = function(location) { + var $modal; + $modal = $('#modal-shortcuts'); + if ($modal.length) { + $modal.modal('toggle'); + return; + } + return $.ajax({ + url: gon.shortcuts_path, + dataType: 'script', + success: function(e) { + var i, l, len, results; + if (location && location.length > 0) { + results = []; + for (i = 0, len = location.length; i < len; i++) { + l = location[i]; + results.push($(l).show()); + } + return results; + } else { + $('.hidden-shortcut').show(); + return $('.js-more-help-button').remove(); + } + } + }); + }; + + Shortcuts.prototype.focusFilter = function(e) { + if (this.filterInput == null) { + this.filterInput = $('input[type=search]', '.nav-controls'); + } + this.filterInput.focus(); + return e.preventDefault(); + }; + + Shortcuts.focusSearch = function(e) { + $('#search').focus(); + return e.preventDefault(); + }; + + return Shortcuts; + + })(); + + $(document).on('click.more_help', '.js-more-help-button', function(e) { + $(this).remove(); + $('.hidden-shortcut').show(); + return e.preventDefault(); + }); + + Mousetrap.stopCallback = (function() { + var defaultStopCallback; + defaultStopCallback = Mousetrap.stopCallback; + return function(e, element, combo) { + if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { + return false; + } else { + return defaultStopCallback.apply(this, arguments); + } + }; + })(); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee deleted file mode 100644 index 8c8689bacee..00000000000 --- a/app/assets/javascripts/shortcuts.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -class @Shortcuts - constructor: (skipResetBindings) -> - @enabledHelp = [] - Mousetrap.reset() if not skipResetBindings - Mousetrap.bind '?', @onToggleHelp - Mousetrap.bind 's', Shortcuts.focusSearch - Mousetrap.bind 'f', (e) => @focusFilter e - Mousetrap.bind ['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview - Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? - - onToggleHelp: (e) => - e.preventDefault() - Shortcuts.toggleHelp(@enabledHelp) - - toggleMarkdownPreview: (e) -> - $(document).triggerHandler('markdown-preview:toggle', [e]) - - @toggleHelp: (location) -> - $modal = $('#modal-shortcuts') - - if $modal.length - $modal.modal('toggle') - return - - $.ajax( - url: gon.shortcuts_path, - dataType: 'script', - success: (e) -> - if location and location.length > 0 - $(l).show() for l in location - else - $('.hidden-shortcut').show() - $('.js-more-help-button').remove() - ) - - focusFilter: (e) -> - @filterInput ?= $('input[type=search]', '.nav-controls') - @filterInput.focus() - e.preventDefault() - - @focusSearch: (e) -> - $('#search').focus() - e.preventDefault() - - -$(document).on 'click.more_help', '.js-more-help-button', (e) -> - $(@).remove() - $('.hidden-shortcut').show() - e.preventDefault() - -Mousetrap.stopCallback = (-> - defaultStopCallback = Mousetrap.stopCallback - - return (e, element, combo) -> - # allowed shortcuts if textarea, input, contenteditable are focused - if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1 - return false - else - return defaultStopCallback.apply(@, arguments) -)() diff --git a/app/assets/javascripts/shortcuts_blob.coffee b/app/assets/javascripts/shortcuts_blob.coffee deleted file mode 100644 index 6d21e5d1150..00000000000 --- a/app/assets/javascripts/shortcuts_blob.coffee +++ /dev/null @@ -1,10 +0,0 @@ -#= require shortcuts - -class @ShortcutsBlob extends Shortcuts - constructor: (skipResetBindings) -> - super skipResetBindings - Mousetrap.bind('y', ShortcutsBlob.copyToClipboard) - - @copyToClipboard: -> - clipboardButton = $('.btn-clipboard') - clipboardButton.click() if clipboardButton diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js new file mode 100644 index 00000000000..b931eab638f --- /dev/null +++ b/app/assets/javascripts/shortcuts_blob.js @@ -0,0 +1,28 @@ + +/*= require shortcuts */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsBlob = (function(superClass) { + extend(ShortcutsBlob, superClass); + + function ShortcutsBlob(skipResetBindings) { + ShortcutsBlob.__super__.constructor.call(this, skipResetBindings); + Mousetrap.bind('y', ShortcutsBlob.copyToClipboard); + } + + ShortcutsBlob.copyToClipboard = function() { + var clipboardButton; + clipboardButton = $('.btn-clipboard'); + if (clipboardButton) { + return clipboardButton.click(); + } + }; + + return ShortcutsBlob; + + })(Shortcuts); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js new file mode 100644 index 00000000000..f7492a2aa5c --- /dev/null +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -0,0 +1,39 @@ + +/*= require shortcuts */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsDashboardNavigation = (function(superClass) { + extend(ShortcutsDashboardNavigation, superClass); + + function ShortcutsDashboardNavigation() { + ShortcutsDashboardNavigation.__super__.constructor.call(this); + Mousetrap.bind('g a', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity'); + }); + Mousetrap.bind('g i', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues'); + }); + Mousetrap.bind('g m', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'); + }); + Mousetrap.bind('g p', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'); + }); + } + + ShortcutsDashboardNavigation.findAndFollowLink = function(selector) { + var link; + link = $(selector).attr('href'); + if (link) { + return window.location = link; + } + }; + + return ShortcutsDashboardNavigation; + + })(Shortcuts); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee deleted file mode 100644 index cca2b8a1fcc..00000000000 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -#= require shortcuts - -class @ShortcutsDashboardNavigation extends Shortcuts - constructor: -> - super() - Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity')) - Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests')) - Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects')) - - @findAndFollowLink: (selector) -> - link = $(selector).attr('href') - if link - window.location = link diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js new file mode 100644 index 00000000000..6c78914d338 --- /dev/null +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -0,0 +1,35 @@ + +/*= require shortcuts_navigation */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsFindFile = (function(superClass) { + extend(ShortcutsFindFile, superClass); + + function ShortcutsFindFile(projectFindFile) { + var _oldStopCallback; + this.projectFindFile = projectFindFile; + ShortcutsFindFile.__super__.constructor.call(this); + _oldStopCallback = Mousetrap.stopCallback; + Mousetrap.stopCallback = (function(_this) { + return function(event, element, combo) { + if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) { + event.preventDefault(); + return false; + } + return _oldStopCallback(event, element, combo); + }; + })(this); + Mousetrap.bind('up', this.projectFindFile.selectRowUp); + Mousetrap.bind('down', this.projectFindFile.selectRowDown); + Mousetrap.bind('esc', this.projectFindFile.goToTree); + Mousetrap.bind('enter', this.projectFindFile.goToBlob); + } + + return ShortcutsFindFile; + + })(ShortcutsNavigation); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee deleted file mode 100644 index 311e80bae19..00000000000 --- a/app/assets/javascripts/shortcuts_find_file.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -#= require shortcuts_navigation - -class @ShortcutsFindFile extends ShortcutsNavigation - constructor: (@projectFindFile) -> - super() - _oldStopCallback = Mousetrap.stopCallback - # override to fire shortcuts action when focus in textbox - Mousetrap.stopCallback = (event, element, combo) => - if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter') - # when press up/down key in textbox, cusor prevent to move to home/end - event.preventDefault() - return false - - return _oldStopCallback(event, element, combo) - - Mousetrap.bind('up', @projectFindFile.selectRowUp) - Mousetrap.bind('down', @projectFindFile.selectRowDown) - Mousetrap.bind('esc', @projectFindFile.goToTree) - Mousetrap.bind('enter', @projectFindFile.goToBlob) diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee deleted file mode 100644 index c93bcf3ceec..00000000000 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ /dev/null @@ -1,53 +0,0 @@ -#= require mousetrap -#= require shortcuts_navigation - -class @ShortcutsIssuable extends ShortcutsNavigation - constructor: (isMergeRequest) -> - super() - Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) - Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) - Mousetrap.bind('r', => - @replyWithSelectedText() - return false - ) - Mousetrap.bind('e', => - @editIssue() - return false - ) - Mousetrap.bind('l', @openSidebarDropdown.bind(@, 'labels')) - - if isMergeRequest - @enabledHelp.push('.hidden-shortcut.merge_requests') - else - @enabledHelp.push('.hidden-shortcut.issues') - - replyWithSelectedText: -> - if window.getSelection - selected = window.getSelection().toString() - replyField = $('.js-main-target-form #note_note') - - return if selected.trim() == "" - - # Put a '>' character before each non-empty line in the selection - quote = _.map selected.split("\n"), (val) -> - "> #{val}\n" if val.trim() != '' - - # If replyField already has some content, add a newline before our quote - separator = replyField.val().trim() != "" and "\n" or '' - - replyField.val (_, current) -> - current + separator + quote.join('') + "\n" - - # Trigger autosave for the added text - replyField.trigger('input') - - # Focus the input field - replyField.focus() - - editIssue: -> - $editBtn = $('.issuable-edit') - Turbolinks.visit($editBtn.attr('href')) - - openSidebarDropdown: (name) -> - sidebar.openDropdown(name) - return false diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js new file mode 100644 index 00000000000..3f3a8a9dfd9 --- /dev/null +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -0,0 +1,75 @@ + +/*= require mousetrap */ + + +/*= require shortcuts_navigation */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsIssuable = (function(superClass) { + extend(ShortcutsIssuable, superClass); + + function ShortcutsIssuable(isMergeRequest) { + ShortcutsIssuable.__super__.constructor.call(this); + Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee')); + Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone')); + Mousetrap.bind('r', (function(_this) { + return function() { + _this.replyWithSelectedText(); + return false; + }; + })(this)); + Mousetrap.bind('e', (function(_this) { + return function() { + _this.editIssue(); + return false; + }; + })(this)); + Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels')); + if (isMergeRequest) { + this.enabledHelp.push('.hidden-shortcut.merge_requests'); + } else { + this.enabledHelp.push('.hidden-shortcut.issues'); + } + } + + ShortcutsIssuable.prototype.replyWithSelectedText = function() { + var quote, replyField, selected, separator; + if (window.getSelection) { + selected = window.getSelection().toString(); + replyField = $('.js-main-target-form #note_note'); + if (selected.trim() === "") { + return; + } + quote = _.map(selected.split("\n"), function(val) { + if (val.trim() !== '') { + return "> " + val + "\n"; + } + }); + separator = replyField.val().trim() !== "" && "\n" || ''; + replyField.val(function(_, current) { + return current + separator + quote.join('') + "\n"; + }); + replyField.trigger('input'); + return replyField.focus(); + } + }; + + ShortcutsIssuable.prototype.editIssue = function() { + var $editBtn; + $editBtn = $('.issuable-edit'); + return Turbolinks.visit($editBtn.attr('href')); + }; + + ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { + sidebar.openDropdown(name); + return false; + }; + + return ShortcutsIssuable; + + })(ShortcutsNavigation); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee deleted file mode 100644 index f39504e0645..00000000000 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require shortcuts - -class @ShortcutsNavigation extends Shortcuts - constructor: -> - super() - Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project')) - Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity')) - Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) - Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) - Mousetrap.bind('g b', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-builds')) - Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) - Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs')) - Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests')) - Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki')) - Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets')) - Mousetrap.bind('i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue')) - @enabledHelp.push('.hidden-shortcut.project') - - @findAndFollowLink: (selector) -> - link = $(selector).attr('href') - if link - window.location = link diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js new file mode 100644 index 00000000000..469e25482bb --- /dev/null +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -0,0 +1,64 @@ + +/*= require shortcuts */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsNavigation = (function(superClass) { + extend(ShortcutsNavigation, superClass); + + function ShortcutsNavigation() { + ShortcutsNavigation.__super__.constructor.call(this); + Mousetrap.bind('g p', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); + }); + Mousetrap.bind('g e', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); + }); + Mousetrap.bind('g f', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); + }); + Mousetrap.bind('g c', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'); + }); + Mousetrap.bind('g b', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'); + }); + Mousetrap.bind('g n', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); + }); + Mousetrap.bind('g g', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'); + }); + Mousetrap.bind('g i', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); + }); + Mousetrap.bind('g m', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'); + }); + Mousetrap.bind('g w', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki'); + }); + Mousetrap.bind('g s', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets'); + }); + Mousetrap.bind('i', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue'); + }); + this.enabledHelp.push('.hidden-shortcut.project'); + } + + ShortcutsNavigation.findAndFollowLink = function(selector) { + var link; + link = $(selector).attr('href'); + if (link) { + return window.location = link; + } + }; + + return ShortcutsNavigation; + + })(Shortcuts); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js new file mode 100644 index 00000000000..fb2b39e757e --- /dev/null +++ b/app/assets/javascripts/shortcuts_network.js @@ -0,0 +1,27 @@ + +/*= require shortcuts_navigation */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsNetwork = (function(superClass) { + extend(ShortcutsNetwork, superClass); + + function ShortcutsNetwork(graph) { + this.graph = graph; + ShortcutsNetwork.__super__.constructor.call(this); + Mousetrap.bind(['left', 'h'], this.graph.scrollLeft); + Mousetrap.bind(['right', 'l'], this.graph.scrollRight); + Mousetrap.bind(['up', 'k'], this.graph.scrollUp); + Mousetrap.bind(['down', 'j'], this.graph.scrollDown); + Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop); + Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom); + this.enabledHelp.push('.hidden-shortcut.network'); + } + + return ShortcutsNetwork; + + })(ShortcutsNavigation); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_network.js.coffee b/app/assets/javascripts/shortcuts_network.js.coffee deleted file mode 100644 index cc95ad7ebfe..00000000000 --- a/app/assets/javascripts/shortcuts_network.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require shortcuts_navigation - -class @ShortcutsNetwork extends ShortcutsNavigation - constructor: (@graph) -> - super() - Mousetrap.bind(['left', 'h'], @graph.scrollLeft) - Mousetrap.bind(['right', 'l'], @graph.scrollRight) - Mousetrap.bind(['up', 'k'], @graph.scrollUp) - Mousetrap.bind(['down', 'j'], @graph.scrollDown) - Mousetrap.bind(['shift+up', 'shift+k'], @graph.scrollTop) - Mousetrap.bind(['shift+down', 'shift+j'], @graph.scrollBottom) - @enabledHelp.push('.hidden-shortcut.network') diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js new file mode 100644 index 00000000000..bd0c1194b36 --- /dev/null +++ b/app/assets/javascripts/sidebar.js @@ -0,0 +1,41 @@ +(function() { + var collapsed, expanded, toggleSidebar; + + collapsed = 'page-sidebar-collapsed'; + + expanded = 'page-sidebar-expanded'; + + toggleSidebar = function() { + $('.page-with-sidebar').toggleClass(collapsed + " " + expanded); + $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded"); + if ($.cookie('pin_nav') === 'true') { + $('.navbar-fixed-top').toggleClass('header-pinned-nav'); + $('.page-with-sidebar').toggleClass('page-sidebar-pinned'); + } + return setTimeout((function() { + var niceScrollBars; + niceScrollBars = $('.nav-sidebar').niceScroll(); + return niceScrollBars.updateScrollBar(); + }), 300); + }; + + $(document).off('click', 'body').on('click', 'body', function(e) { + var $nav, $target, $toggle, pageExpanded; + if ($.cookie('pin_nav') !== 'true') { + $target = $(e.target); + $nav = $target.closest('.sidebar-wrapper'); + pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded'); + $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle'); + if ($nav.length === 0 && pageExpanded && $toggle.length === 0) { + $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded'); + return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded'); + } + } + }); + + $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) { + e.preventDefault(); + return toggleSidebar(); + }); + +}).call(this); diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee deleted file mode 100644 index 68009e58645..00000000000 --- a/app/assets/javascripts/sidebar.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -collapsed = 'page-sidebar-collapsed' -expanded = 'page-sidebar-expanded' - -toggleSidebar = -> - $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") - $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded") - - if $.cookie('pin_nav') is 'true' - $('.navbar-fixed-top').toggleClass('header-pinned-nav') - $('.page-with-sidebar').toggleClass('page-sidebar-pinned') - - setTimeout ( -> - niceScrollBars = $('.nav-sidebar').niceScroll(); - niceScrollBars.updateScrollBar(); - ), 300 - -$(document) - .off 'click', 'body' - .on 'click', 'body', (e) -> - unless $.cookie('pin_nav') is 'true' - $target = $(e.target) - $nav = $target.closest('.sidebar-wrapper') - pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded') - $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle') - - if $nav.length is 0 and pageExpanded and $toggle.length is 0 - $('.page-with-sidebar') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - - $('.navbar-fixed-top') - .toggleClass('header-collapsed header-expanded') - -$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> - e.preventDefault() - - toggleSidebar() -) diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js new file mode 100644 index 00000000000..b9ae497b0e5 --- /dev/null +++ b/app/assets/javascripts/single_file_diff.js @@ -0,0 +1,77 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.SingleFileDiff = (function() { + var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; + + WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; + + LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; + + ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; + + COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'; + + function SingleFileDiff(file) { + this.file = file; + this.toggleDiff = bind(this.toggleDiff, this); + this.content = $('.diff-content', this.file); + this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); + this.isOpen = !this.diffForPath; + if (this.diffForPath) { + this.collapsedContent = this.content; + this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); + this.content = null; + this.collapsedContent.after(this.loadingContent); + } else { + this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); + this.content.after(this.collapsedContent); + } + this.collapsedContent.on('click', this.toggleDiff); + $('.file-title > a', this.file).on('click', this.toggleDiff); + } + + SingleFileDiff.prototype.toggleDiff = function(e) { + this.isOpen = !this.isOpen; + if (!this.isOpen && !this.hasError) { + this.content.hide(); + return this.collapsedContent.show(); + } else if (this.content) { + this.collapsedContent.hide(); + return this.content.show(); + } else { + return this.getContentHTML(); + } + }; + + SingleFileDiff.prototype.getContentHTML = function() { + this.collapsedContent.hide(); + this.loadingContent.show(); + $.get(this.diffForPath, (function(_this) { + return function(data) { + _this.loadingContent.hide(); + if (data.html) { + _this.content = $(data.html); + _this.content.syntaxHighlight(); + } else { + _this.hasError = true; + _this.content = $(ERROR_HTML); + } + return _this.collapsedContent.after(_this.content); + }; + })(this)); + }; + + return SingleFileDiff; + + })(); + + $.fn.singleFileDiff = function() { + return this.each(function() { + if (!$.data(this, 'singleFileDiff')) { + return $.data(this, 'singleFileDiff', new SingleFileDiff(this)); + } + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/single_file_diff.js.coffee b/app/assets/javascripts/single_file_diff.js.coffee deleted file mode 100644 index f3e225c3728..00000000000 --- a/app/assets/javascripts/single_file_diff.js.coffee +++ /dev/null @@ -1,54 +0,0 @@ -class @SingleFileDiff - - WRAPPER = '<div class="diff-content diff-wrap-lines"></div>' - LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>' - ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>' - COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>' - - constructor: (@file) -> - @content = $('.diff-content', @file) - @diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path' - @isOpen = !@diffForPath - - if @diffForPath - @collapsedContent = @content - @loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide() - @content = null - @collapsedContent.after(@loadingContent) - else - @collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide() - @content.after(@collapsedContent) - - @collapsedContent.on 'click', @toggleDiff - - $('.file-title > a', @file).on 'click', @toggleDiff - - toggleDiff: (e) => - @isOpen = !@isOpen - if not @isOpen and not @hasError - @content.hide() - @collapsedContent.show() - else if @content - @collapsedContent.hide() - @content.show() - else - @getContentHTML() - - getContentHTML: -> - @collapsedContent.hide() - @loadingContent.show() - $.get @diffForPath, (data) => - @loadingContent.hide() - if data.html - @content = $(data.html) - @content.syntaxHighlight() - else - @hasError = true - @content = $(ERROR_HTML) - @collapsedContent.after(@content) - return - -$.fn.singleFileDiff = -> - return @each -> - if not $.data this, 'singleFileDiff' - $.data this, 'singleFileDiff', new SingleFileDiff this diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js new file mode 100644 index 00000000000..10509313c12 --- /dev/null +++ b/app/assets/javascripts/star.js @@ -0,0 +1,31 @@ +(function() { + this.Star = (function() { + function Star() { + $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { + var $starIcon, $starSpan, $this, toggleStar; + $this = $(this); + $starSpan = $this.find('span'); + $starIcon = $this.find('i'); + toggleStar = function(isStarred) { + $this.parent().find('.star-count').text(data.star_count); + if (isStarred) { + $starSpan.removeClass('starred').text('Star'); + gl.utils.updateTooltipTitle($this, 'Star project'); + $starIcon.removeClass('fa-star').addClass('fa-star-o'); + } else { + $starSpan.addClass('starred').text('Unstar'); + gl.utils.updateTooltipTitle($this, 'Unstar project'); + $starIcon.removeClass('fa-star-o').addClass('fa-star'); + } + }; + toggleStar($starSpan.hasClass('starred')); + }).on('ajax:error', function(e, xhr, status, error) { + new Flash('Star toggle failed. Try again later.', 'alert'); + }); + } + + return Star; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee deleted file mode 100644 index 01b28171f72..00000000000 --- a/app/assets/javascripts/star.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -class @Star - constructor: -> - $('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) -> - $this = $(this) - $starSpan = $this.find('span') - $starIcon = $this.find('i') - - toggleStar = (isStarred) -> - $this.parent().find('.star-count').text data.star_count - if isStarred - $starSpan.removeClass('starred').text 'Star' - gl.utils.updateTooltipTitle $this, 'Star project' - $starIcon.removeClass('fa-star').addClass 'fa-star-o' - else - $starSpan.addClass('starred').text 'Unstar' - gl.utils.updateTooltipTitle $this, 'Unstar project' - $starIcon.removeClass('fa-star-o').addClass 'fa-star' - return - - toggleStar $starSpan.hasClass('starred') - return - ).on 'ajax:error', (e, xhr, status, error) -> - new Flash('Star toggle failed. Try again later.', 'alert') - return diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js new file mode 100644 index 00000000000..5e3c5983d75 --- /dev/null +++ b/app/assets/javascripts/subscription.js @@ -0,0 +1,41 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Subscription = (function() { + function Subscription(container) { + this.toggleSubscription = bind(this.toggleSubscription, this); + var $container; + $container = $(container); + this.url = $container.attr('data-url'); + this.subscribe_button = $container.find('.js-subscribe-button'); + this.subscription_status = $container.find('.subscription-status'); + this.subscribe_button.unbind('click').click(this.toggleSubscription); + } + + Subscription.prototype.toggleSubscription = function(event) { + var action, btn, current_status; + btn = $(event.currentTarget); + action = btn.find('span').text(); + current_status = this.subscription_status.attr('data-status'); + btn.addClass('disabled'); + return $.post(this.url, (function(_this) { + return function() { + var status; + btn.removeClass('disabled'); + status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; + _this.subscription_status.attr('data-status', status); + action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; + btn.find('span').text(action); + _this.subscription_status.find('>div').toggleClass('hidden'); + if (btn.attr('data-original-title')) { + return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); + } + }; + })(this)); + }; + + return Subscription; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee deleted file mode 100644 index 08d494aba9f..00000000000 --- a/app/assets/javascripts/subscription.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -class @Subscription - constructor: (container) -> - $container = $(container) - @url = $container.attr('data-url') - @subscribe_button = $container.find('.js-subscribe-button') - @subscription_status = $container.find('.subscription-status') - @subscribe_button.unbind('click').click(@toggleSubscription) - - toggleSubscription: (event) => - btn = $(event.currentTarget) - action = btn.find('span').text() - current_status = @subscription_status.attr('data-status') - btn.addClass('disabled') - - $.post @url, => - btn.removeClass('disabled') - status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed' - @subscription_status.attr('data-status', status) - action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' - btn.find('span').text(action) - @subscription_status.find('>div').toggleClass('hidden') - - if btn.attr('data-original-title') - btn.tooltip('hide') - .attr('data-original-title', action) - .tooltip('fixTitle') diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js new file mode 100644 index 00000000000..d6c219603d1 --- /dev/null +++ b/app/assets/javascripts/subscription_select.js @@ -0,0 +1,35 @@ +(function() { + this.SubscriptionSelect = (function() { + function SubscriptionSelect() { + $('.js-subscription-event').each(function(i, el) { + var fieldName; + fieldName = $(el).data("field-name"); + return $(el).glDropdown({ + selectable: true, + fieldName: fieldName, + toggleLabel: (function(_this) { + return function(selected, el, instance) { + var $item, label; + label = 'Subscription'; + $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); + } + return label; + }; + })(this), + clicked: function(item, $el, e) { + return e.preventDefault(); + }, + id: function(obj, el) { + return $(el).data("id"); + } + }); + }); + } + + return SubscriptionSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/subscription_select.js.coffee b/app/assets/javascripts/subscription_select.js.coffee deleted file mode 100644 index e5eb7a50d80..00000000000 --- a/app/assets/javascripts/subscription_select.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -class @SubscriptionSelect - constructor: -> - $('.js-subscription-event').each (i, el) -> - fieldName = $(el).data("field-name") - - $(el).glDropdown( - selectable: true - fieldName: fieldName - toggleLabel: (selected, el, instance) => - label = 'Subscription' - $item = instance.dropdown.find('.is-active') - label = $item.text() if $item.length - label - clicked: (item, $el, e)-> - e.preventDefault() - id: (obj, el) -> - $(el).data("id") - ) diff --git a/app/assets/javascripts/syntax_highlight.coffee b/app/assets/javascripts/syntax_highlight.coffee deleted file mode 100644 index 980f0232d10..00000000000 --- a/app/assets/javascripts/syntax_highlight.coffee +++ /dev/null @@ -1,20 +0,0 @@ -# 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> -# -$.fn.syntaxHighlight = -> - if $(this).hasClass('js-syntax-highlight') - # Given the element itself, apply highlighting - $(this).addClass(gon.user_color_scheme) - else - # Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight') - $children.syntaxHighlight() if $children.length - -$(document).on 'ready page:load', -> - $('.js-syntax-highlight').syntaxHighlight() diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js new file mode 100644 index 00000000000..dba62638c78 --- /dev/null +++ b/app/assets/javascripts/syntax_highlight.js @@ -0,0 +1,18 @@ +(function() { + $.fn.syntaxHighlight = function() { + var $children; + if ($(this).hasClass('js-syntax-highlight')) { + return $(this).addClass(gon.user_color_scheme); + } else { + $children = $(this).find('.js-syntax-highlight'); + if ($children.length) { + return $children.syntaxHighlight(); + } + } + }; + + $(document).on('ready page:load', function() { + return $('.js-syntax-highlight').syntaxHighlight(); + }); + +}).call(this); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js new file mode 100644 index 00000000000..6e677fa8cc6 --- /dev/null +++ b/app/assets/javascripts/todos.js @@ -0,0 +1,144 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Todos = (function() { + function Todos(opts) { + var ref; + if (opts == null) { + opts = {}; + } + this.allDoneClicked = bind(this.allDoneClicked, this); + this.doneClicked = bind(this.doneClicked, this); + this.el = (ref = opts.el) != null ? ref : $('.js-todos-options'); + this.perPage = this.el.data('perPage'); + this.clearListeners(); + this.initBtnListeners(); + } + + Todos.prototype.clearListeners = function() { + $('.done-todo').off('click'); + $('.js-todos-mark-all').off('click'); + return $('.todo').off('click'); + }; + + Todos.prototype.initBtnListeners = function() { + $('.done-todo').on('click', this.doneClicked); + $('.js-todos-mark-all').on('click', this.allDoneClicked); + return $('.todo').on('click', this.goToTodoUrl); + }; + + Todos.prototype.doneClicked = function(e) { + var $this; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(e.currentTarget); + $this.disable(); + return $.ajax({ + type: 'POST', + url: $this.attr('href'), + dataType: 'json', + data: { + '_method': 'delete' + }, + success: (function(_this) { + return function(data) { + _this.redirectIfNeeded(data.count); + _this.clearDone($this.closest('li')); + return _this.updateBadges(data); + }; + })(this) + }); + }; + + Todos.prototype.allDoneClicked = function(e) { + var $this; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(e.currentTarget); + $this.disable(); + return $.ajax({ + type: 'POST', + url: $this.attr('href'), + dataType: 'json', + data: { + '_method': 'delete' + }, + success: (function(_this) { + return function(data) { + $this.remove(); + $('.js-todos-list').remove(); + return _this.updateBadges(data); + }; + })(this) + }); + }; + + Todos.prototype.clearDone = function($row) { + var $ul; + $ul = $row.closest('ul'); + $row.remove(); + if (!$ul.find('li').length) { + return $ul.parents('.panel').remove(); + } + }; + + Todos.prototype.updateBadges = function(data) { + $('.todos-pending .badge, .todos-pending-count').text(data.count); + return $('.todos-done .badge').text(data.done_count); + }; + + Todos.prototype.getTotalPages = function() { + return this.el.data('totalPages'); + }; + + Todos.prototype.getCurrentPage = function() { + return this.el.data('currentPage'); + }; + + Todos.prototype.getTodosPerPage = function() { + return this.el.data('perPage'); + }; + + Todos.prototype.redirectIfNeeded = function(total) { + var currPage, currPages, newPages, pageParams, url; + currPages = this.getTotalPages(); + currPage = this.getCurrentPage(); + if (!total) { + location.reload(); + return; + } + if (!currPages) { + return; + } + newPages = Math.ceil(total / this.getTodosPerPage()); + url = location.href; + if (newPages !== currPages) { + if (currPages > 1 && currPage === currPages) { + pageParams = { + page: currPages - 1 + }; + url = gl.utils.mergeUrlParams(pageParams, url); + } + return Turbolinks.visit(url); + } + }; + + Todos.prototype.goToTodoUrl = function(e) { + var todoLink; + todoLink = $(this).data('url'); + if (!todoLink) { + return; + } + if (e.metaKey || e.which === 2) { + e.preventDefault(); + return window.open(todoLink, '_blank'); + } else { + return Turbolinks.visit(todoLink); + } + }; + + return Todos; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee deleted file mode 100644 index 10bef96f43d..00000000000 --- a/app/assets/javascripts/todos.js.coffee +++ /dev/null @@ -1,110 +0,0 @@ -class @Todos - constructor: (opts = {}) -> - { - @el = $('.js-todos-options') - } = opts - - @perPage = @el.data('perPage') - - @clearListeners() - @initBtnListeners() - - clearListeners: -> - $('.done-todo').off('click') - $('.js-todos-mark-all').off('click') - $('.todo').off('click') - - initBtnListeners: -> - $('.done-todo').on('click', @doneClicked) - $('.js-todos-mark-all').on('click', @allDoneClicked) - $('.todo').on('click', @goToTodoUrl) - - doneClicked: (e) => - e.preventDefault() - e.stopImmediatePropagation() - - $this = $(e.currentTarget) - $this.disable() - - $.ajax - type: 'POST' - url: $this.attr('href') - dataType: 'json' - data: '_method': 'delete' - success: (data) => - @redirectIfNeeded data.count - @clearDone $this.closest('li') - @updateBadges data - - allDoneClicked: (e) => - e.preventDefault() - e.stopImmediatePropagation() - - $this = $(e.currentTarget) - $this.disable() - - $.ajax - type: 'POST' - url: $this.attr('href') - dataType: 'json' - data: '_method': 'delete' - success: (data) => - $this.remove() - $('.js-todos-list').remove() - @updateBadges data - - clearDone: ($row) -> - $ul = $row.closest('ul') - $row.remove() - - if not $ul.find('li').length - $ul.parents('.panel').remove() - - updateBadges: (data) -> - $('.todos-pending .badge, .todos-pending-count').text data.count - $('.todos-done .badge').text data.done_count - - getTotalPages: -> - @el.data('totalPages') - - getCurrentPage: -> - @el.data('currentPage') - - getTodosPerPage: -> - @el.data('perPage') - - redirectIfNeeded: (total) -> - currPages = @getTotalPages() - currPage = @getCurrentPage() - - # Refresh if no remaining Todos - if not total - location.reload() - return - - # Do nothing if no pagination - return if not currPages - - newPages = Math.ceil(total / @getTodosPerPage()) - url = location.href # Includes query strings - - # If new total of pages is different than we have now - if newPages isnt currPages - # Redirect to previous page if there's one available - if currPages > 1 and currPage is currPages - pageParams = - page: currPages - 1 - url = gl.utils.mergeUrlParams(pageParams, url) - - Turbolinks.visit(url) - - goToTodoUrl: (e)-> - todoLink = $(this).data('url') - return unless todoLink - - # Allow Meta-Click or Mouse3-click to open in a new tab - if e.metaKey or e.which is 2 - e.preventDefault() - window.open(todoLink,'_blank') - else - Turbolinks.visit(todoLink) diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js new file mode 100644 index 00000000000..78e159a7ed9 --- /dev/null +++ b/app/assets/javascripts/tree.js @@ -0,0 +1,65 @@ +(function() { + this.TreeView = (function() { + function TreeView() { + this.initKeyNav(); + $(".tree-content-holder .tree-item").on('click', function(e) { + var $clickedEl, path; + $clickedEl = $(e.target); + path = $('.tree-item-file-name a', this).attr('href'); + if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { + if (e.metaKey || e.which === 2) { + e.preventDefault(); + return window.open(path, '_blank'); + } else { + return Turbolinks.visit(path); + } + } + }); + $('span.log_loading:first').removeClass('hide'); + } + + TreeView.prototype.initKeyNav = function() { + var li, liSelected; + li = $("tr.tree-item"); + liSelected = null; + return $('body').keydown(function(e) { + var next, path; + if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) { + return false; + } + if (e.which === 40) { + if (liSelected) { + next = liSelected.next(); + if (next.length > 0) { + liSelected.removeClass("selected"); + liSelected = next.addClass("selected"); + } + } else { + liSelected = li.eq(0).addClass("selected"); + } + return $(liSelected).focus(); + } else if (e.which === 38) { + if (liSelected) { + next = liSelected.prev(); + if (next.length > 0) { + liSelected.removeClass("selected"); + liSelected = next.addClass("selected"); + } + } else { + liSelected = li.last().addClass("selected"); + } + return $(liSelected).focus(); + } else if (e.which === 13) { + path = $('.tree-item.selected .tree-item-file-name a').attr('href'); + if (path) { + return Turbolinks.visit(path); + } + } + }); + }; + + return TreeView; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee deleted file mode 100644 index 83de584f2d9..00000000000 --- a/app/assets/javascripts/tree.js.coffee +++ /dev/null @@ -1,50 +0,0 @@ -class @TreeView - constructor: -> - @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', (e) -> - $clickedEl = $(e.target) - path = $('.tree-item-file-name a', this).attr('href') - - if not $clickedEl.is('a') and not $clickedEl.is('.str-truncated') - if e.metaKey or e.which is 2 - e.preventDefault() - window.open path, '_blank' - else - Turbolinks.visit path - - # Show the "Loading commit data" for only the first element - $('span.log_loading:first').removeClass('hide') - - initKeyNav: -> - li = $("tr.tree-item") - liSelected = null - $('body').keydown (e) -> - if $("input:focus").length > 0 && (e.which == 38 || e.which == 40) - return false - - if e.which is 40 - if liSelected - next = liSelected.next() - if next.length > 0 - liSelected.removeClass "selected" - liSelected = next.addClass("selected") - else - liSelected = li.eq(0).addClass("selected") - - $(liSelected).focus() - else if e.which is 38 - if liSelected - next = liSelected.prev() - if next.length > 0 - liSelected.removeClass "selected" - liSelected = next.addClass("selected") - else - liSelected = li.last().addClass("selected") - - $(liSelected).focus() - else if e.which is 13 - path = $('.tree-item.selected .tree-item-file-name a').attr('href') - if path then Turbolinks.visit(path) diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js new file mode 100644 index 00000000000..9ba847fb0c2 --- /dev/null +++ b/app/assets/javascripts/u2f/authenticate.js @@ -0,0 +1,89 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.U2FAuthenticate = (function() { + function U2FAuthenticate(container, u2fParams) { + this.container = container; + this.renderNotSupported = bind(this.renderNotSupported, this); + this.renderAuthenticated = bind(this.renderAuthenticated, this); + this.renderError = bind(this.renderError, this); + this.renderInProgress = bind(this.renderInProgress, this); + this.renderSetup = bind(this.renderSetup, this); + this.renderTemplate = bind(this.renderTemplate, this); + this.authenticate = bind(this.authenticate, this); + this.start = bind(this.start, this); + this.appId = u2fParams.app_id; + this.challenge = u2fParams.challenge; + this.signRequests = u2fParams.sign_requests.map(function(request) { + return _(request).omit('challenge'); + }); + } + + U2FAuthenticate.prototype.start = function() { + if (U2FUtil.isU2FSupported()) { + return this.renderSetup(); + } else { + return this.renderNotSupported(); + } + }; + + U2FAuthenticate.prototype.authenticate = function() { + return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) { + return function(response) { + var error; + if (response.errorCode) { + error = new U2FError(response.errorCode); + return _this.renderError(error); + } else { + return _this.renderAuthenticated(JSON.stringify(response)); + } + }; + })(this), 10); + }; + + U2FAuthenticate.prototype.templates = { + "notSupported": "#js-authenticate-u2f-not-supported", + "setup": '#js-authenticate-u2f-setup', + "inProgress": '#js-authenticate-u2f-in-progress', + "error": '#js-authenticate-u2f-error', + "authenticated": '#js-authenticate-u2f-authenticated' + }; + + U2FAuthenticate.prototype.renderTemplate = function(name, params) { + var template, templateString; + templateString = $(this.templates[name]).html(); + template = _.template(templateString); + return this.container.html(template(params)); + }; + + U2FAuthenticate.prototype.renderSetup = function() { + this.renderTemplate('setup'); + return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress); + }; + + U2FAuthenticate.prototype.renderInProgress = function() { + this.renderTemplate('inProgress'); + return this.authenticate(); + }; + + U2FAuthenticate.prototype.renderError = function(error) { + this.renderTemplate('error', { + error_message: error.message() + }); + return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + }; + + U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { + this.renderTemplate('authenticated'); + return this.container.find("#js-device-response").val(deviceResponse); + }; + + U2FAuthenticate.prototype.renderNotSupported = function() { + return this.renderTemplate('notSupported'); + }; + + return U2FAuthenticate; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee deleted file mode 100644 index 918c0a560fd..00000000000 --- a/app/assets/javascripts/u2f/authenticate.js.coffee +++ /dev/null @@ -1,75 +0,0 @@ -# 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 - -class @U2FAuthenticate - constructor: (@container, u2fParams) -> - @appId = u2fParams.app_id - @challenge = u2fParams.challenge - - # 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 - @signRequests = u2fParams.sign_requests.map (request) -> _(request).omit('challenge') - - start: () => - if U2FUtil.isU2FSupported() - @renderSetup() - else - @renderNotSupported() - - authenticate: () => - u2f.sign(@appId, @challenge, @signRequests, (response) => - if response.errorCode - error = new U2FError(response.errorCode) - @renderError(error); - else - @renderAuthenticated(JSON.stringify(response)) - , 10) - - ############# - # Rendering # - ############# - - templates: { - "notSupported": "#js-authenticate-u2f-not-supported", - "setup": '#js-authenticate-u2f-setup', - "inProgress": '#js-authenticate-u2f-in-progress', - "error": '#js-authenticate-u2f-error', - "authenticated": '#js-authenticate-u2f-authenticated' - } - - renderTemplate: (name, params) => - templateString = $(@templates[name]).html() - template = _.template(templateString) - @container.html(template(params)) - - renderSetup: () => - @renderTemplate('setup') - @container.find('#js-login-u2f-device').on('click', @renderInProgress) - - renderInProgress: () => - @renderTemplate('inProgress') - @authenticate() - - renderError: (error) => - @renderTemplate('error', {error_message: error.message()}) - @container.find('#js-u2f-try-again').on('click', @renderSetup) - - renderAuthenticated: (deviceResponse) => - @renderTemplate('authenticated') - # Prefer to do this instead of interpolating using Underscore templates - # because of JSON escaping issues. - @container.find("#js-device-response").val(deviceResponse) - - renderNotSupported: () => - @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js new file mode 100644 index 00000000000..bc48c67c4f2 --- /dev/null +++ b/app/assets/javascripts/u2f/error.js @@ -0,0 +1,27 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.U2FError = (function() { + function U2FError(errorCode) { + this.errorCode = errorCode; + this.message = bind(this.message, this); + this.httpsDisabled = window.location.protocol !== 'https:'; + console.error("U2F Error Code: " + this.errorCode); + } + + U2FError.prototype.message = function() { + switch (false) { + case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled): + return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."; + case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE: + return "This device has already been registered with us."; + default: + return "There was a problem communicating with your device."; + } + }; + + return U2FError; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee deleted file mode 100644 index 1a2fc3e757f..00000000000 --- a/app/assets/javascripts/u2f/error.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -class @U2FError - constructor: (@errorCode) -> - @httpsDisabled = (window.location.protocol isnt 'https:') - console.error("U2F Error Code: #{@errorCode}") - - message: () => - switch - when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled) - "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." - when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE - "This device has already been registered with us." - else - "There was a problem communicating with your device." diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js new file mode 100644 index 00000000000..c87e0840df3 --- /dev/null +++ b/app/assets/javascripts/u2f/register.js @@ -0,0 +1,87 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.U2FRegister = (function() { + function U2FRegister(container, u2fParams) { + this.container = container; + this.renderNotSupported = bind(this.renderNotSupported, this); + this.renderRegistered = bind(this.renderRegistered, this); + this.renderError = bind(this.renderError, this); + this.renderInProgress = bind(this.renderInProgress, this); + this.renderSetup = bind(this.renderSetup, this); + this.renderTemplate = bind(this.renderTemplate, this); + this.register = bind(this.register, this); + this.start = bind(this.start, this); + this.appId = u2fParams.app_id; + this.registerRequests = u2fParams.register_requests; + this.signRequests = u2fParams.sign_requests; + } + + U2FRegister.prototype.start = function() { + if (U2FUtil.isU2FSupported()) { + return this.renderSetup(); + } else { + return this.renderNotSupported(); + } + }; + + U2FRegister.prototype.register = function() { + return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) { + return function(response) { + var error; + if (response.errorCode) { + error = new U2FError(response.errorCode); + return _this.renderError(error); + } else { + return _this.renderRegistered(JSON.stringify(response)); + } + }; + })(this), 10); + }; + + U2FRegister.prototype.templates = { + "notSupported": "#js-register-u2f-not-supported", + "setup": '#js-register-u2f-setup', + "inProgress": '#js-register-u2f-in-progress', + "error": '#js-register-u2f-error', + "registered": '#js-register-u2f-registered' + }; + + U2FRegister.prototype.renderTemplate = function(name, params) { + var template, templateString; + templateString = $(this.templates[name]).html(); + template = _.template(templateString); + return this.container.html(template(params)); + }; + + U2FRegister.prototype.renderSetup = function() { + this.renderTemplate('setup'); + return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); + }; + + U2FRegister.prototype.renderInProgress = function() { + this.renderTemplate('inProgress'); + return this.register(); + }; + + U2FRegister.prototype.renderError = function(error) { + this.renderTemplate('error', { + error_message: error.message() + }); + return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + }; + + U2FRegister.prototype.renderRegistered = function(deviceResponse) { + this.renderTemplate('registered'); + return this.container.find("#js-device-response").val(deviceResponse); + }; + + U2FRegister.prototype.renderNotSupported = function() { + return this.renderTemplate('notSupported'); + }; + + return U2FRegister; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee deleted file mode 100644 index 74472cfa120..00000000000 --- a/app/assets/javascripts/u2f/register.js.coffee +++ /dev/null @@ -1,63 +0,0 @@ -# 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 - -class @U2FRegister - constructor: (@container, u2fParams) -> - @appId = u2fParams.app_id - @registerRequests = u2fParams.register_requests - @signRequests = u2fParams.sign_requests - - start: () => - if U2FUtil.isU2FSupported() - @renderSetup() - else - @renderNotSupported() - - register: () => - u2f.register(@appId, @registerRequests, @signRequests, (response) => - if response.errorCode - error = new U2FError(response.errorCode) - @renderError(error); - else - @renderRegistered(JSON.stringify(response)) - , 10) - - ############# - # Rendering # - ############# - - templates: { - "notSupported": "#js-register-u2f-not-supported", - "setup": '#js-register-u2f-setup', - "inProgress": '#js-register-u2f-in-progress', - "error": '#js-register-u2f-error', - "registered": '#js-register-u2f-registered' - } - - renderTemplate: (name, params) => - templateString = $(@templates[name]).html() - template = _.template(templateString) - @container.html(template(params)) - - renderSetup: () => - @renderTemplate('setup') - @container.find('#js-setup-u2f-device').on('click', @renderInProgress) - - renderInProgress: () => - @renderTemplate('inProgress') - @register() - - renderError: (error) => - @renderTemplate('error', {error_message: error.message()}) - @container.find('#js-u2f-try-again').on('click', @renderSetup) - - renderRegistered: (deviceResponse) => - @renderTemplate('registered') - # Prefer to do this instead of interpolating using Underscore templates - # because of JSON escaping issues. - @container.find("#js-device-response").val(deviceResponse) - - renderNotSupported: () => - @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js new file mode 100644 index 00000000000..907e640161a --- /dev/null +++ b/app/assets/javascripts/u2f/util.js @@ -0,0 +1,13 @@ +(function() { + this.U2FUtil = (function() { + function U2FUtil() {} + + U2FUtil.isU2FSupported = function() { + return window.u2f; + }; + + return U2FUtil; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/util.js.coffee b/app/assets/javascripts/u2f/util.js.coffee deleted file mode 100644 index 5ef324f609d..00000000000 --- a/app/assets/javascripts/u2f/util.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -class @U2FUtil - @isU2FSupported: -> - window.u2f diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js new file mode 100644 index 00000000000..b46390ad8f4 --- /dev/null +++ b/app/assets/javascripts/user.js @@ -0,0 +1,31 @@ +(function() { + this.User = (function() { + function User(opts) { + this.opts = opts; + $('.profile-groups-avatars').tooltip({ + "placement": "top" + }); + this.initTabs(); + $('.hide-project-limit-message').on('click', function(e) { + var path; + path = '/'; + $.cookie('hide_project_limit_message', 'false', { + path: path + }); + $(this).parents('.project-limit-message').remove(); + return e.preventDefault(); + }); + } + + User.prototype.initTabs = function() { + return new UserTabs({ + parentEl: '.user-profile', + action: this.opts.action + }); + }; + + return User; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee deleted file mode 100644 index 2882a90d118..00000000000 --- a/app/assets/javascripts/user.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -class @User - constructor: (@opts) -> - $('.profile-groups-avatars').tooltip("placement": "top") - - @initTabs() - - $('.hide-project-limit-message').on 'click', (e) -> - path = '/' - $.cookie('hide_project_limit_message', 'false', { path: path }) - $(@).parents('.project-limit-message').remove() - e.preventDefault() - - initTabs: -> - new UserTabs( - parentEl: '.user-profile' - action: @opts.action - ) diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js new file mode 100644 index 00000000000..e5e75701fee --- /dev/null +++ b/app/assets/javascripts/user_tabs.js @@ -0,0 +1,119 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.UserTabs = (function() { + function UserTabs(opts) { + 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); + if (typeof this.parentEl === 'string') { + this.parentEl = $(this.parentEl); + } + this._location = location; + 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; + } + this.actions = Object.keys(this.loaded); + this.bindEvents(); + if (this.action === 'show') { + this.action = this.defaultAction; + } + this.activateTab(this.action); + } + + UserTabs.prototype.bindEvents = function() { + 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); + }; + + UserTabs.prototype.tabShown = function(event) { + var $target, action, source; + $target = $(event.target); + action = $target.data('action'); + source = $target.attr('href'); + this.setTab(source, action); + return this.setCurrentAction(action); + }; + + UserTabs.prototype.activateTab = function(action) { + return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show'); + }; + + UserTabs.prototype.setTab = function(source, action) { + if (this.loaded[action] === true) { + return; + } + if (action === 'activity') { + this.loadActivities(source); + } + if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') { + return this.loadTab(source, action); + } + }; + + UserTabs.prototype.loadTab = function(source, action) { + return $.ajax({ + beforeSend: (function(_this) { + return function() { + return _this.toggleLoading(true); + }; + })(this), + complete: (function(_this) { + return function() { + return _this.toggleLoading(false); + }; + })(this), + dataType: 'json', + type: 'GET', + url: source + ".json", + success: (function(_this) { + return function(data) { + var tabSelector; + tabSelector = 'div#' + action; + _this.parentEl.find(tabSelector).html(data.html); + _this.loaded[action] = true; + return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + }; + })(this) + }); + }; + + UserTabs.prototype.loadActivities = function(source) { + var $calendarWrap; + if (this.loaded['activity'] === true) { + return; + } + $calendarWrap = this.parentEl.find('.user-calendar'); + $calendarWrap.load($calendarWrap.data('href')); + new Activities(); + return this.loaded['activity'] = true; + }; + + UserTabs.prototype.toggleLoading = function(status) { + return this.parentEl.find('.loading-status .loading').toggle(status); + }; + + UserTabs.prototype.setCurrentAction = function(action) { + var new_state, regExp; + regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$'); + new_state = this._location.pathname; + new_state = new_state.replace(/\/+$/, ""); + new_state = new_state.replace(regExp, ''); + if (action !== this.defaultAction) { + new_state += "/" + action; + } + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + return new_state; + }; + + return UserTabs; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee deleted file mode 100644 index 29dad21faed..00000000000 --- a/app/assets/javascripts/user_tabs.js.coffee +++ /dev/null @@ -1,156 +0,0 @@ -# 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> -# -class @UserTabs - constructor: (opts) -> - { - @action = 'activity' - @defaultAction = 'activity' - @parentEl = $(document) - } = opts - - # Make jQuery object if selector is provided - @parentEl = $(@parentEl) if typeof @parentEl is 'string' - - # Store the `location` object, allowing for easier stubbing in tests - @_location = location - - # Set tab states - @loaded = {} - for item in @parentEl.find('.nav-links a') - @loaded[$(item).attr 'data-action'] = false - - # Actions - @actions = Object.keys @loaded - - @bindEvents() - - # Set active tab - @action = @defaultAction if @action is 'show' - @activateTab(@action) - - bindEvents: -> - # Toggle event listeners - @parentEl - .off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]' - .on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown - - tabShown: (event) => - $target = $(event.target) - action = $target.data('action') - source = $target.attr('href') - - @setTab(source, action) - @setCurrentAction(action) - - activateTab: (action) -> - @parentEl.find(".nav-links .js-#{action}-tab a").tab('show') - - setTab: (source, action) -> - return if @loaded[action] is true - - if action is 'activity' - @loadActivities(source) - - if action in ['groups', 'contributed', 'projects', 'snippets'] - @loadTab(source, action) - - loadTab: (source, action) -> - $.ajax - beforeSend: => @toggleLoading(true) - complete: => @toggleLoading(false) - dataType: 'json' - type: 'GET' - url: "#{source}.json" - success: (data) => - tabSelector = 'div#' + action - @parentEl.find(tabSelector).html(data.html) - @loaded[action] = true - - # Fix tooltips - gl.utils.localTimeAgo($('.js-timeago', tabSelector)) - - loadActivities: (source) -> - return if @loaded['activity'] is true - - $calendarWrap = @parentEl.find('.user-calendar') - $calendarWrap.load($calendarWrap.data('href')) - - new Activities() - @loaded['activity'] = true - - toggleLoading: (status) -> - @parentEl.find('.loading-status .loading').toggle(status) - - setCurrentAction: (action) -> - # Remove possible actions from URL - regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$') - new_state = @_location.pathname - new_state = new_state.replace(/\/+$/, "") # remove trailing slashes - new_state = new_state.replace(regExp, '') - - # Append the new action if we're on a tab other than 'activity' - unless action == @defaultAction - new_state += "/#{action}" - - # Ensure parameters and hash come along for the ride - new_state += @_location.search + @_location.hash - - history.replaceState {turbolinks: true, url: new_state}, document.title, new_state - - new_state diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js new file mode 100644 index 00000000000..8b3dbf5f5ae --- /dev/null +++ b/app/assets/javascripts/users/calendar.js @@ -0,0 +1,192 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Calendar = (function() { + function Calendar(timestamps, calendar_activities_path) { + var group, i; + this.calendar_activities_path = calendar_activities_path; + this.clickDay = bind(this.clickDay, this); + this.currentSelectedDate = ''; + this.daySpace = 1; + this.daySize = 15; + this.daySizeWithSpace = this.daySize + (this.daySpace * 2); + this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + this.months = []; + this.timestampsTmp = []; + i = 0; + group = 0; + _.each(timestamps, (function(_this) { + return function(count, date) { + var day, innerArray, newDate; + newDate = new Date(parseInt(date) * 1000); + day = newDate.getDay(); + if ((day === 0 && i !== 0) || i === 0) { + _this.timestampsTmp.push([]); + group++; + } + innerArray = _this.timestampsTmp[group - 1]; + innerArray.push({ + count: count, + date: newDate, + day: day + }); + return i++; + }; + })(this)); + this.colorKey = this.initColorKey(); + this.color = this.initColor(); + this.renderSvg(group); + this.renderDays(); + this.renderMonths(); + this.renderDayTitles(); + this.renderKey(); + this.initTooltips(); + } + + Calendar.prototype.renderSvg = function(group) { + return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar'); + }; + + Calendar.prototype.renderDays = function() { + return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) { + return function(group, i) { + _.each(group, function(stamp, a) { + var lastMonth, lastMonthX, month, x; + if (a === 0 && stamp.day === 0) { + month = stamp.date.getMonth(); + x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace; + lastMonth = _.last(_this.months); + if (lastMonth != null) { + lastMonthX = lastMonth.x; + } + if (lastMonth == null) { + return _this.months.push({ + month: month, + x: x + }); + } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) { + return _this.months.push({ + month: month, + x: x + }); + } + } + }); + return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)"; + }; + })(this)).selectAll('rect').data(function(stamp) { + return stamp; + }).enter().append('rect').attr('x', '0').attr('y', (function(_this) { + return function(stamp, i) { + return _this.daySizeWithSpace * stamp.day; + }; + })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) { + return function(stamp) { + var contribText, date, dateText; + date = new Date(stamp.date); + contribText = 'No contributions'; + if (stamp.count > 0) { + contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); + } + dateText = dateFormat(date, 'mmm d, yyyy'); + return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; + }; + })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { + return function(stamp) { + if (stamp.count !== 0) { + return _this.color(Math.min(stamp.count, 40)); + } else { + return '#ededed'; + } + }; + })(this)).attr('data-container', 'body').on('click', this.clickDay); + }; + + Calendar.prototype.renderDayTitles = function() { + var days; + days = [ + { + text: 'M', + y: 29 + (this.daySizeWithSpace * 1) + }, { + text: 'W', + y: 29 + (this.daySizeWithSpace * 3) + }, { + text: 'F', + y: 29 + (this.daySizeWithSpace * 5) + } + ]; + return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) { + return day.y; + }).text(function(day) { + return day.text; + }).attr('class', 'user-contrib-text'); + }; + + Calendar.prototype.renderMonths = function() { + return this.svg.append('g').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) { + return date.x; + }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) { + return function(date) { + return _this.monthNames[date.month]; + }; + })(this)); + }; + + Calendar.prototype.renderKey = function() { + var keyColors; + keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + return this.svg.append('g').attr('transform', "translate(18, " + (this.daySizeWithSpace * 8 + 16) + ")").selectAll('rect').data(keyColors).enter().append('rect').attr('width', this.daySize).attr('height', this.daySize).attr('x', (function(_this) { + return function(color, i) { + return _this.daySizeWithSpace * i; + }; + })(this)).attr('y', 0).attr('fill', function(color) { + return color; + }); + }; + + Calendar.prototype.initColor = function() { + var colorRange; + colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange); + }; + + Calendar.prototype.initColorKey = function() { + return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); + }; + + Calendar.prototype.clickDay = function(stamp) { + var formatted_date; + if (this.currentSelectedDate !== stamp.date) { + this.currentSelectedDate = stamp.date; + formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate(); + return $.ajax({ + url: this.calendar_activities_path, + data: { + date: formatted_date + }, + cache: false, + dataType: 'html', + beforeSend: function() { + return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>'); + }, + success: function(data) { + return $('.user-calendar-activities').html(data); + } + }); + } else { + return $('.user-calendar-activities').html(''); + } + }; + + Calendar.prototype.initTooltips = function() { + return $('.js-contrib-calendar .js-tooltip').tooltip({ + html: true + }); + }; + + return Calendar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee deleted file mode 100644 index c49ba5186f2..00000000000 --- a/app/assets/javascripts/users/calendar.js.coffee +++ /dev/null @@ -1,194 +0,0 @@ -class @Calendar - constructor: (timestamps, @calendar_activities_path) -> - @currentSelectedDate = '' - @daySpace = 1 - @daySize = 15 - @daySizeWithSpace = @daySize + (@daySpace * 2) - @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - @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 - @timestampsTmp = [] - i = 0 - group = 0 - _.each timestamps, (count, date) => - newDate = new Date parseInt(date) * 1000 - day = newDate.getDay() - - # Create a new group array if this is the first day of the week - # or if is first object - if (day is 0 and i isnt 0) or i is 0 - @timestampsTmp.push [] - group++ - - innerArray = @timestampsTmp[group-1] - - # Push to the inner array the values that will be used to render map - innerArray.push - count: count - date: newDate - day: day - - i++ - - # Init color functions - @colorKey = @initColorKey() - @color = @initColor() - - # Init the svg element - @renderSvg(group) - @renderDays() - @renderMonths() - @renderDayTitles() - @renderKey() - - @initTooltips() - - renderSvg: (group) -> - @svg = d3.select '.js-contrib-calendar' - .append 'svg' - .attr 'width', (group + 1) * @daySizeWithSpace - .attr 'height', 167 - .attr 'class', 'contrib-calendar' - - renderDays: -> - @svg.selectAll 'g' - .data @timestampsTmp - .enter() - .append 'g' - .attr 'transform', (group, i) => - _.each group, (stamp, a) => - if a is 0 and stamp.day is 0 - month = stamp.date.getMonth() - x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace - lastMonth = _.last(@months) - if lastMonth? - lastMonthX = lastMonth.x - - if !lastMonth? - @months.push - month: month - x: x - else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX - @months.push - month: month - x: x - - "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)" - .selectAll 'rect' - .data (stamp) -> - stamp - .enter() - .append 'rect' - .attr 'x', '0' - .attr 'y', (stamp, i) => - (@daySizeWithSpace * stamp.day) - .attr 'width', @daySize - .attr 'height', @daySize - .attr 'title', (stamp) => - date = new Date(stamp.date) - contribText = 'No contributions' - - if stamp.count > 0 - contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}" - - dateText = dateFormat(date, 'mmm d, yyyy') - - "#{contribText}<br />#{gl.utils.getDayName(date)} #{dateText}" - .attr 'class', 'user-contrib-cell js-tooltip' - .attr 'fill', (stamp) => - if stamp.count isnt 0 - @color(Math.min(stamp.count, 40)) - else - '#ededed' - .attr 'data-container', 'body' - .on 'click', @clickDay - - renderDayTitles: -> - days = [{ - text: 'M' - y: 29 + (@daySizeWithSpace * 1) - }, { - text: 'W' - y: 29 + (@daySizeWithSpace * 3) - }, { - text: 'F' - y: 29 + (@daySizeWithSpace * 5) - }] - @svg.append 'g' - .selectAll 'text' - .data days - .enter() - .append 'text' - .attr 'text-anchor', 'middle' - .attr 'x', 8 - .attr 'y', (day) -> - day.y - .text (day) -> - day.text - .attr 'class', 'user-contrib-text' - - renderMonths: -> - @svg.append 'g' - .selectAll 'text' - .data @months - .enter() - .append 'text' - .attr 'x', (date) -> - date.x - .attr 'y', 10 - .attr 'class', 'user-contrib-text' - .text (date) => - @monthNames[date.month] - - renderKey: -> - keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] - @svg.append 'g' - .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})" - .selectAll 'rect' - .data keyColors - .enter() - .append 'rect' - .attr 'width', @daySize - .attr 'height', @daySize - .attr 'x', (color, i) => - @daySizeWithSpace * i - .attr 'y', 0 - .attr 'fill', (color) -> - color - - initColor: -> - colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] - d3.scale - .threshold() - .domain([0, 10, 20, 30]) - .range(colorRange) - - initColorKey: -> - d3.scale - .linear() - .range(['#acd5f2', '#254e77']) - .domain([0, 3]) - - clickDay: (stamp) => - if @currentSelectedDate isnt stamp.date - @currentSelectedDate = stamp.date - formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate() - - $.ajax - url: @calendar_activities_path - data: - date: formatted_date - cache: false - dataType: 'html' - beforeSend: -> - $('.user-calendar-activities').html '<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>' - success: (data) -> - $('.user-calendar-activities').html data - else - $('.user-calendar-activities').html '' - - initTooltips: -> - $('.js-contrib-calendar .js-tooltip').tooltip - html: true diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js new file mode 100644 index 00000000000..b95faadc8e7 --- /dev/null +++ b/app/assets/javascripts/users/users_bundle.js @@ -0,0 +1,7 @@ + +/*= require_tree . */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/users/users_bundle.js.coffee b/app/assets/javascripts/users/users_bundle.js.coffee deleted file mode 100644 index 91cacfece46..00000000000 --- a/app/assets/javascripts/users/users_bundle.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -# -#= require_tree . diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js new file mode 100644 index 00000000000..64a29d36cdf --- /dev/null +++ b/app/assets/javascripts/users_select.js @@ -0,0 +1,342 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + slice = [].slice; + + this.UsersSelect = (function() { + function UsersSelect(currentUser) { + this.users = bind(this.users, this); + this.user = bind(this.user, this); + this.usersPath = "/autocomplete/users.json"; + this.userPath = "/autocomplete/users/:id.json"; + if (currentUser != null) { + this.currentUser = JSON.parse(currentUser); + } + $('.js-user-search').each((function(_this) { + return function(i, dropdown) { + var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; + $dropdown = $(dropdown); + _this.projectId = $dropdown.data('project-id'); + _this.showCurrentUser = $dropdown.data('current-user'); + showNullUser = $dropdown.data('null-user'); + showAnyUser = $dropdown.data('any-user'); + firstUser = $dropdown.data('first-user'); + _this.authorId = $dropdown.data('author-id'); + selectedId = $dropdown.data('selected'); + defaultLabel = $dropdown.data('default-label'); + issueURL = $dropdown.data('issueUpdate'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + abilityName = $dropdown.data('ability-name'); + $value = $block.find('.value'); + $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + $loading = $block.find('.block-loading').fadeOut(); + $block.on('click', '.js-assign-yourself', function(e) { + e.preventDefault(); + return assignTo(_this.currentUser.id); + }); + assignTo = function(selected) { + var data; + data = {}; + data[abilityName] = {}; + data[abilityName].assignee_id = selected != null ? selected : null; + $loading.fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + dataType: 'json', + url: issueURL, + data: data + }).done(function(data) { + var user; + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + $selectbox.hide(); + if (data.assignee) { + user = { + name: data.assignee.name, + username: data.assignee.username, + avatar: data.assignee.avatar_url + }; + } else { + user = { + name: 'Unassigned', + username: '', + avatar: '' + }; + } + $value.html(assigneeTemplate(user)); + $collapsedSidebar.attr('title', user.name).tooltip('fixTitle'); + return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + }); + }; + collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); + assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); + return $dropdown.glDropdown({ + data: function(term, callback) { + var isAuthorFilter; + isAuthorFilter = $('.js-author-search'); + return _this.users(term, function(users) { + var anyUser, index, j, len, name, obj, showDivider; + if (term.length === 0) { + showDivider = 0; + if (firstUser) { + for (index = j = 0, len = users.length; j < len; index = ++j) { + obj = users[index]; + if (obj.username === firstUser) { + users.splice(index, 1); + users.unshift(obj); + break; + } + } + } + if (showNullUser) { + showDivider += 1; + users.unshift({ + beforeDivider: true, + name: 'Unassigned', + id: 0 + }); + } + if (showAnyUser) { + showDivider += 1; + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + beforeDivider: true, + name: name, + id: null + }; + users.unshift(anyUser); + } + } + if (showDivider) { + users.splice(showDivider, 0, "divider"); + } + return callback(users); + }); + }, + filterable: true, + filterRemote: true, + search: { + fields: ['name', 'username'] + }, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel: function(selected) { + if (selected && 'id' in selected) { + if (selected.text) { + return selected.text; + } else { + return selected.name; + } + } else { + return defaultLabel; + } + }, + inputId: 'issue_assignee_id', + hidden: function(e) { + $selectbox.hide(); + return $value.css('display', ''); + }, + clicked: function(user) { + var isIssueIndex, isMRIndex, page, selected; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = (page === page && page === 'projects:merge_requests:index'); + if ($dropdown.hasClass('js-filter-bulk-update')) { + return; + } + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + selectedId = user.id; + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else { + selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); + return assignTo(selected); + } + }, + renderRow: function(user) { + var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; + username = user.username ? "@" + user.username : ""; + avatar = user.avatar_url ? user.avatar_url : false; + selected = user.id === selectedId ? "is-active" : ""; + img = ""; + if (user.beforeDivider != null) { + "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>"; + } else { + if (avatar) { + img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />"; + } + } + 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>"; + if (username === '') { + listWithUserName = ''; + } + return listWithName + listWithUserName + listClosingTags; + } + }); + }; + })(this)); + $('.ajax-users-select').each((function(_this) { + return function(i, select) { + var firstUser, showAnyUser, showEmailUser, showNullUser; + _this.projectId = $(select).data('project-id'); + _this.groupId = $(select).data('group-id'); + _this.showCurrentUser = $(select).data('current-user'); + _this.authorId = $(select).data('author-id'); + showNullUser = $(select).data('null-user'); + showAnyUser = $(select).data('any-user'); + showEmailUser = $(select).data('email-user'); + firstUser = $(select).data('first-user'); + return $(select).select2({ + placeholder: "Search for a user", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return _this.users(query.term, function(users) { + var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; + data = { + results: users + }; + if (query.term.length === 0) { + if (firstUser) { + ref = data.results; + for (index = j = 0, len = ref.length; j < len; index = ++j) { + obj = ref[index]; + if (obj.username === firstUser) { + data.results.splice(index, 1); + data.results.unshift(obj); + break; + } + } + } + if (showNullUser) { + nullUser = { + name: 'Unassigned', + id: 0 + }; + data.results.unshift(nullUser); + } + if (showAnyUser) { + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + name: name, + id: null + }; + data.results.unshift(anyUser); + } + } + if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { + emailUser = { + name: "Invite \"" + query.term + "\"", + username: query.term, + id: query.term + }; + data.results.unshift(emailUser); + } + return query.callback(data); + }); + }, + initSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.initSelection.apply(_this, args); + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-users-dropdown", + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } + + UsersSelect.prototype.initSelection = function(element, callback) { + var id, nullUser; + id = $(element).val(); + if (id === "0") { + nullUser = { + name: 'Unassigned' + }; + return callback(nullUser); + } else if (id !== "") { + return this.user(id, callback); + } + }; + + UsersSelect.prototype.formatResult = function(user) { + var avatar; + if (user.avatar_url) { + avatar = user.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>"; + }; + + UsersSelect.prototype.formatSelection = function(user) { + return user.name; + }; + + UsersSelect.prototype.user = function(user_id, callback) { + var url; + url = this.buildUrl(this.userPath); + url = url.replace(':id', user_id); + return $.ajax({ + url: url, + dataType: "json" + }).done(function(user) { + return callback(user); + }); + }; + + UsersSelect.prototype.users = function(query, callback) { + var url; + url = this.buildUrl(this.usersPath); + return $.ajax({ + url: url, + data: { + search: query, + per_page: 20, + active: true, + project_id: this.projectId, + group_id: this.groupId, + current_user: this.showCurrentUser, + author_id: this.authorId + }, + dataType: "json" + }).done(function(users) { + return callback(users); + }); + }; + + UsersSelect.prototype.buildUrl = function(url) { + if (gon.relative_url_root != null) { + url = gon.relative_url_root.replace(/\/$/, '') + url; + } + return url; + }; + + return UsersSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee deleted file mode 100644 index 344be811e0d..00000000000 --- a/app/assets/javascripts/users_select.js.coffee +++ /dev/null @@ -1,330 +0,0 @@ -class @UsersSelect - constructor: (currentUser) -> - @usersPath = "/autocomplete/users.json" - @userPath = "/autocomplete/users/:id.json" - if currentUser? - @currentUser = JSON.parse(currentUser) - - $('.js-user-search').each (i, dropdown) => - $dropdown = $(dropdown) - @projectId = $dropdown.data('project-id') - @showCurrentUser = $dropdown.data('current-user') - showNullUser = $dropdown.data('null-user') - showAnyUser = $dropdown.data('any-user') - firstUser = $dropdown.data('first-user') - @authorId = $dropdown.data('author-id') - selectedId = $dropdown.data('selected') - defaultLabel = $dropdown.data('default-label') - issueURL = $dropdown.data('issueUpdate') - $selectbox = $dropdown.closest('.selectbox') - $block = $selectbox.closest('.block') - abilityName = $dropdown.data('ability-name') - $value = $block.find('.value') - $collapsedSidebar = $block.find('.sidebar-collapsed-user') - $loading = $block.find('.block-loading').fadeOut() - - $block.on('click', '.js-assign-yourself', (e) => - e.preventDefault() - assignTo(@currentUser.id) - ) - - assignTo = (selected) -> - data = {} - data[abilityName] = {} - data[abilityName].assignee_id = if selected? then selected else null - $loading - .fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $.ajax( - type: 'PUT' - dataType: 'json' - url: issueURL - data: data - ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $loading.fadeOut() - $selectbox.hide() - - if data.assignee - user = - name: data.assignee.name - username: data.assignee.username - avatar: data.assignee.avatar_url - else - user = - name: 'Unassigned' - username: '' - avatar: '' - $value.html(assigneeTemplate(user)) - - $collapsedSidebar - .attr('title', user.name) - .tooltip('fixTitle') - - $collapsedSidebar.html(collapsedAssigneeTemplate(user)) - - - collapsedAssigneeTemplate = _.template( - '<% if( avatar ) { %> - <a class="author_link" href="/u/<%- username %>"> - <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> - </a> - <% } else { %> - <i class="fa fa-user"></i> - <% } %>' - ) - - assigneeTemplate = _.template( - '<% if (username) { %> - <a class="author_link bold" href="/u/<%- username %>"> - <% if( avatar ) { %> - <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> - <% } %> - <span class="author"><%- name %></span> - <span class="username"> - @<%- username %> - </span> - </a> - <% } else { %> - <span class="no-value assign-yourself"> - No assignee - - <a href="#" class="js-assign-yourself"> - assign yourself - </a> - </span> - <% } %>' - ) - - $dropdown.glDropdown( - data: (term, callback) => - isAuthorFilter = $('.js-author-search') - - @users term, (users) => - if term.length is 0 - showDivider = 0 - - if firstUser - # Move current user to the front of the list - for obj, index in users - if obj.username == firstUser - users.splice(index, 1) - users.unshift(obj) - break - - if showNullUser - showDivider += 1 - users.unshift( - beforeDivider: true - name: 'Unassigned', - id: 0 - ) - - if showAnyUser - showDivider += 1 - name = showAnyUser - name = 'Any User' if name == true - anyUser = { - beforeDivider: true - name: name, - id: null - } - users.unshift(anyUser) - - if showDivider - users.splice(showDivider, 0, "divider") - - # Send the data back - callback users - filterable: true - filterRemote: true - search: - fields: ['name', 'username'] - selectable: true - fieldName: $dropdown.data('field-name') - - toggleLabel: (selected) -> - if selected && 'id' of selected - if selected.text then selected.text else selected.name - else - defaultLabel - - inputId: 'issue_assignee_id' - - hidden: (e) -> - $selectbox.hide() - # display:block overrides the hide-collapse rule - $value.css('display', '') - - clicked: (user) -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - if $dropdown.hasClass('js-filter-bulk-update') - return - - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedId = user.id - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' - $dropdown.closest('form').submit() - else - selected = $dropdown - .closest('.selectbox') - .find("input[name='#{$dropdown.data('field-name')}']").val() - assignTo(selected) - - renderRow: (user) -> - username = if user.username then "@#{user.username}" else "" - avatar = if user.avatar_url then user.avatar_url else false - selected = if user.id is selectedId then "is-active" else "" - img = "" - - if user.beforeDivider? - "<li> - <a href='#' class='#{selected}'> - #{user.name} - </a> - </li>" - else - if avatar - 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>" - - - if username is '' - listWithUserName = '' - - listWithName + listWithUserName + listClosingTags - ) - - $('.ajax-users-select').each (i, select) => - @projectId = $(select).data('project-id') - @groupId = $(select).data('group-id') - @showCurrentUser = $(select).data('current-user') - @authorId = $(select).data('author-id') - showNullUser = $(select).data('null-user') - showAnyUser = $(select).data('any-user') - showEmailUser = $(select).data('email-user') - firstUser = $(select).data('first-user') - - $(select).select2 - placeholder: "Search for a user" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) => - @users query.term, (users) => - data = { results: users } - - if query.term.length == 0 - if firstUser - # Move current user to the front of the list - for obj, index in data.results - if obj.username == firstUser - data.results.splice(index, 1) - data.results.unshift(obj) - break - - if showNullUser - nullUser = { - name: 'Unassigned', - id: 0 - } - data.results.unshift(nullUser) - - if showAnyUser - name = showAnyUser - name = 'Any User' if name == true - anyUser = { - name: name, - id: null - } - data.results.unshift(anyUser) - - if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/) - emailUser = { - name: "Invite \"#{query.term}\"", - username: query.term, - id: query.term - } - data.results.unshift(emailUser) - - query.callback(data) - - initSelection: (args...) => - @initSelection(args...) - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-users-dropdown" - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - initSelection: (element, callback) -> - id = $(element).val() - if id == "0" - nullUser = { name: 'Unassigned' } - callback(nullUser) - else if id != "" - @user(id, callback) - - formatResult: (user) -> - if user.avatar_url - avatar = user.avatar_url - else - avatar = gon.default_avatar_url - - "<div class='user-result #{'no-username' unless user.username}'> - <div class='user-image'><img class='avatar s24' src='#{avatar}'></div> - <div class='user-name'>#{user.name}</div> - <div class='user-username'>#{user.username || ""}</div> - </div>" - - formatSelection: (user) -> - user.name - - user: (user_id, callback) => - url = @buildUrl(@userPath) - url = url.replace(':id', user_id) - - $.ajax( - url: url - dataType: "json" - ).done (user) -> - callback(user) - - # Return users list. Filtered by query - # Only active users retrieved - users: (query, callback) => - url = @buildUrl(@usersPath) - - $.ajax( - url: url - data: - search: query - per_page: 20 - active: true - project_id: @projectId - group_id: @groupId - current_user: @showCurrentUser - author_id: @authorId - dataType: "json" - ).done (users) -> - callback(users) - - buildUrl: (url) -> - url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root? - return url diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js new file mode 100644 index 00000000000..35401231fbf --- /dev/null +++ b/app/assets/javascripts/wikis.js @@ -0,0 +1,37 @@ + +/*= require latinise */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Wikis = (function() { + function Wikis() { + this.slugify = bind(this.slugify, this); + $('.new-wiki-page').on('submit', (function(_this) { + return function(e) { + var field, path, slug; + $('[data-error~=slug]').addClass('hidden'); + field = $('#new_wiki_path'); + slug = _this.slugify(field.val()); + if (slug.length > 0) { + path = field.attr('data-wikis-path'); + location.href = path + '/' + slug; + return e.preventDefault(); + } + }; + })(this)); + } + + Wikis.prototype.dasherize = function(value) { + return value.replace(/[_\s]+/g, '-'); + }; + + Wikis.prototype.slugify = function(value) { + return this.dasherize(value.trim().toLowerCase().latinise()); + }; + + return Wikis; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee deleted file mode 100644 index 1ee827f1fa3..00000000000 --- a/app/assets/javascripts/wikis.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -#= require latinise - -class @Wikis - constructor: -> - $('.new-wiki-page').on 'submit', (e) => - $('[data-error~=slug]').addClass('hidden') - field = $('#new_wiki_path') - slug = @slugify(field.val()) - - if (slug.length > 0) - path = field.attr('data-wikis-path') - location.href = path + '/' + slug - e.preventDefault() - - dasherize: (value) -> - value.replace(/[_\s]+/g, '-') - - slugify: (value) => - @dasherize(value.trim().toLowerCase().latinise()) diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js new file mode 100644 index 00000000000..71236c6a27d --- /dev/null +++ b/app/assets/javascripts/zen_mode.js @@ -0,0 +1,80 @@ + +/*= provides zen_mode:enter */ + + +/*= provides zen_mode:leave */ + + +/*= require jquery.scrollTo */ + + +/*= require dropzone */ + + +/*= require mousetrap */ + + +/*= require mousetrap/pause */ + +(function() { + this.ZenMode = (function() { + function ZenMode() { + this.active_backdrop = null; + this.active_textarea = null; + $(document).on('click', '.js-zen-enter', function(e) { + e.preventDefault(); + return $(e.currentTarget).trigger('zen_mode:enter'); + }); + $(document).on('click', '.js-zen-leave', function(e) { + e.preventDefault(); + return $(e.currentTarget).trigger('zen_mode:leave'); + }); + $(document).on('zen_mode:enter', (function(_this) { + return function(e) { + return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop')); + }; + })(this)); + $(document).on('zen_mode:leave', (function(_this) { + return function(e) { + return _this.exit(); + }; + })(this)); + $(document).on('keydown', function(e) { + if (e.keyCode === 27) { + e.preventDefault(); + return $(document).trigger('zen_mode:leave'); + } + }); + } + + ZenMode.prototype.enter = function(backdrop) { + Mousetrap.pause(); + this.active_backdrop = $(backdrop); + this.active_backdrop.addClass('fullscreen'); + this.active_textarea = this.active_backdrop.find('textarea'); + this.active_textarea.removeAttr('style'); + return this.active_textarea.focus(); + }; + + ZenMode.prototype.exit = function() { + if (this.active_textarea) { + Mousetrap.unpause(); + this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); + this.scrollTo(this.active_textarea); + this.active_textarea = null; + this.active_backdrop = null; + return Dropzone.forElement('.div-dropzone').enable(); + } + }; + + ZenMode.prototype.scrollTo = function(zen_area) { + return $.scrollTo(zen_area, 0, { + offset: -150 + }); + }; + + return ZenMode; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee deleted file mode 100644 index 99f35ecfb0f..00000000000 --- a/app/assets/javascripts/zen_mode.js.coffee +++ /dev/null @@ -1,80 +0,0 @@ -# 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 -# -class @ZenMode - constructor: -> - @active_backdrop = null - @active_textarea = null - - $(document).on 'click', '.js-zen-enter', (e) -> - e.preventDefault() - $(e.currentTarget).trigger('zen_mode:enter') - - $(document).on 'click', '.js-zen-leave', (e) -> - e.preventDefault() - $(e.currentTarget).trigger('zen_mode:leave') - - $(document).on 'zen_mode:enter', (e) => - @enter($(e.target).closest('.md-area').find('.zen-backdrop')) - $(document).on 'zen_mode:leave', (e) => - @exit() - - $(document).on 'keydown', (e) -> - if e.keyCode == 27 # Esc - e.preventDefault() - $(document).trigger('zen_mode:leave') - - enter: (backdrop) -> - Mousetrap.pause() - - @active_backdrop = $(backdrop) - @active_backdrop.addClass('fullscreen') - - @active_textarea = @active_backdrop.find('textarea') - - # Prevent a user-resized textarea from persisting to fullscreen - @active_textarea.removeAttr('style') - @active_textarea.focus() - - exit: -> - if @active_textarea - Mousetrap.unpause() - - @active_textarea.closest('.zen-backdrop').removeClass('fullscreen') - - @scrollTo(@active_textarea) - - @active_textarea = null - @active_backdrop = null - - Dropzone.forElement('.div-dropzone').enable() - - scrollTo: (zen_area) -> - $.scrollTo(zen_area, 0, offset: -150) diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 8b6ddf8ba18..c79b22d4d21 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -5,6 +5,7 @@ height: 40px; padding: 0; @include border-radius($avatar_radius); + border: 1px solid rgba(0, 0, 0, .1); &.avatar-inline { float: none; @@ -15,8 +16,9 @@ &.s24 { margin-right: 4px; } } - &.group-avatar, &.project-avatar, &.avatar-tile { + &.avatar-tile { @include border-radius(0); + border: none; } &.s16 { width: 16px; height: 16px; margin-right: 6px; } @@ -43,12 +45,12 @@ &.s16 { font-size: 12px; line-height: 1.33; } &.s24 { font-size: 14px; line-height: 1.8; } &.s26 { font-size: 20px; line-height: 1.33; } - &.s32 { font-size: 20px; line-height: 32px; } - &.s40 { font-size: 16px; line-height: 40px; } - &.s60 { font-size: 32px; line-height: 60px; } - &.s70 { font-size: 34px; line-height: 70px; } - &.s90 { font-size: 36px; line-height: 90px; } - &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } - &.s140 { font-size: 72px; line-height: 140px; } - &.s160 { font-size: 96px; line-height: 160px; } + &.s32 { font-size: 20px; line-height: 30px; } + &.s40 { font-size: 16px; line-height: 38px; } + &.s60 { font-size: 32px; line-height: 58px; } + &.s70 { font-size: 34px; line-height: 68px; } + &.s90 { font-size: 36px; line-height: 88px; } + &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } + &.s140 { font-size: 72px; line-height: 138px; } + &.s160 { font-size: 96px; line-height: 158px; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 5d3273ea64d..96565da1bc9 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -98,13 +98,30 @@ .md { &.md-preview-holder { - code { - white-space: pre-wrap; - word-break: keep-all; - } - + // Reset ul style types since we're nested inside a ul already @include bulleted-list; } + + // On diffs code should wrap nicely and not overflow + code { + white-space: pre-wrap; + word-break: keep-all; + } + + hr { + // Darken 'whitesmoke' a bit to make it more visible in note bodies + border-color: darken(#f5f5f5, 8%); + margin: 10px 0; + } + + // Border around images in issue and MR comments. + img:not(.emoji) { + border: 1px solid $table-border-gray; + padding: 5px; + margin: 5px 0; + // Ensure that image does not exceed viewport + max-height: calc(100vh - 100px); + } } .toolbar-group { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 99a2cd306cf..e26f8f7080d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -53,6 +53,14 @@ left: 70px; } } + + .nav-links { + svg { + position: relative; + top: 2px; + margin-right: 3px; + } + } } .build-header { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 35ab28b3fea..bbe0c6c5f1f 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -68,6 +68,12 @@ } } +.ci-status-link { + svg { + overflow: visible; + } +} + .commit-box { border-top: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index cf7567513ec..42928ee279c 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -36,10 +36,6 @@ .dash-project-avatar { float: left; - - .avatar { - @include border-radius(50%); - } } .dash-project-access-icon { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index a2145956eb5..5c336bb1c7e 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -176,3 +176,11 @@ } } } + +// hide event scope (namespace + project) where it is not necessary +.project-activity { + .event-scope { + display: none; + } +} + diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index ded437625ee..7a50bc9c832 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -269,7 +269,7 @@ .issuable-header-btn { background: $gray-normal; border: 1px solid $border-gray-normal; - + &:hover { background: $gray-dark; border: 1px solid $border-gray-dark; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 5254faf723d..db295935b00 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -64,12 +64,21 @@ margin-right: 4px; position: relative; top: 1px; + overflow: visible; } &.ci-success { color: $gl-success; } + &.ci-success_with_warnings { + color: $gl-success; + + i { + color: $gl-warning; + } + } + &.ci-skipped { background-color: #eee; color: #888; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ac8c02b59dc..a2b5437e503 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -91,34 +91,11 @@ ul.notes { // Reset ul style types since we're nested inside a ul already @include bulleted-list; - // On diffs code should wrap nicely and not overflow - code { - white-space: pre-wrap; - } - ul.task-list { ul:not(.task-list) { padding-left: 1.3em; } } - - hr { - // Darken 'whitesmoke' a bit to make it more visible in note bodies - border-color: darken(#f5f5f5, 8%); - margin: 10px 0; - } - - code { - word-break: keep-all; - } - - // Border around images in issue and MR comments. - img:not(.emoji) { - border: 1px solid $table-border-gray; - padding: 5px; - margin: 5px 0; - max-height: calc(100vh - 100px); - } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a404f108dc4..c58e2ffe7f5 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -29,9 +29,18 @@ } } +.pipeline-holder { + width: 100%; + overflow: auto; +} + .table.builds { min-width: 1200px; + &.pipeline { + min-width: 650px; + } + tr { th { padding: 16px 8px; @@ -76,7 +85,7 @@ svg { height: 14px; - width: auto; + width: 14px; vertical-align: middle; fill: $table-text-gray; } @@ -93,7 +102,7 @@ .commit-title { margin-top: 4px; - max-width: 320px; + max-width: 300px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -138,6 +147,11 @@ height: 18px; width: 18px; vertical-align: middle; + overflow: visible; + } + + .light { + width: 3px; } } @@ -153,7 +167,7 @@ svg { width: 12px; - height: auto; + height: 12px; vertical-align: middle; margin-right: 4px; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index a22d4b6f6be..587f2d9f3c1 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -15,7 +15,8 @@ border-color: $gl-danger; } - &.ci-success { + &.ci-success, + &.ci-success_with_warnings { color: $gl-success; border-color: $gl-success; } @@ -48,6 +49,7 @@ position: relative; top: 1px; margin: 0 3px; + overflow: visible; } } @@ -57,9 +59,12 @@ .ci-status-icon-failed { color: $gl-danger; } - .ci-status-icon-pending { + + .ci-status-icon-pending, + .ci-status-icon-success_with_warning { color: $gl-warning; } + .ci-status-icon-running { color: $blue-normal; } @@ -70,3 +75,11 @@ color: $gl-gray; } } + +.visible-xs-inline { + .ci-status-link { + position: relative; + top: 2px; + left: 5px; + } +} |