diff options
322 files changed, 14719 insertions, 13302 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 90c09619f8c..00000000000 --- a/app/assets/javascripts/admin.js.coffee +++ /dev/null @@ -1,51 +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) - - showBlacklistType = -> - if $("input[name='blacklist_type']:checked").val() == 'file' - $('.blacklist-file').show() - $('.blacklist-raw').hide() - else - $('.blacklist-file').hide() - $('.blacklist-raw').show() - - $("input[name='blacklist_type']").click showBlacklistType - showBlacklistType() 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 5ab82c39fcd..00000000000 --- a/app/assets/javascripts/files_comment_button.js.coffee +++ /dev/null @@ -1,98 +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' - 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) - .off 'mouseover', LINE_COLUMN_CLASSES - .off 'mouseleave', LINE_COLUMN_CLASSES - .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 - - if @VIEW_TYPE is 'inline' - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find ".#{LINE_CONTENT_CLASS}" - else - return $(hoveredElement).next ".#{LINE_CONTENT_CLASS}" - - getButtonParent: (hoveredElement) -> - if @VIEW_TYPE is 'inline' - return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS - - hoveredElement.parent().find ".#{OLD_LINE_CLASS}" - else - return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS - - $(hoveredElement).prev ".#{LINE_NUMBER_CLASS}" - - 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 7086ece29b8..00000000000 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ /dev/null @@ -1,638 +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) => - @parseData data - - unless @filterInput.val() is '' - selector = '.dropdown-content li:not(.divider):visible' - - if @dropdown.find('.dropdown-toggle-page').length - selector = ".dropdown-page-one #{selector}" - - $(selector, @dropdown) - .first() - .find('a') - .addClass('is-focused') - - currentIndex = 0 - - - # 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 963a0550c35..00000000000 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ /dev/null @@ -1,143 +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) -> - switch status - when 'success' - 'passed' - when 'success_with_warnings' - 'passed with warnings' - 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", "success_with_warnings", "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", "success_with_warnings" - @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 d4de712f88c..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_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.diff_discussion_html - - # remove the note (will be added again below) - row.next().find(".note").remove() - - # Before that, the container didn't exist - discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") - - # Add note to 'Changes' page discussions - discussionContainer.append note_html - - # Init discussion on 'Discussion' page if it is merge request page - if $('body').attr('data-page').indexOf('projects:merge_request') is 0 - $('ul.main-notes-list') - .append(note.discussion_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 0c95301e380..00000000000 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ /dev/null @@ -1,175 +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) -> - - return if $(e.currentTarget).hasClass('dont-change-state') - - 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/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js new file mode 100644 index 00000000000..b48026c3b77 --- /dev/null +++ b/spec/javascripts/application_spec.js @@ -0,0 +1,32 @@ + +/*= require lib/utils/common_utils */ + +(function() { + describe('Application', function() { + return describe('disable buttons', function() { + fixture.preload('application.html'); + beforeEach(function() { + return fixture.load('application.html'); + }); + it('should prevent default action for disabled buttons', function() { + var $button, isClicked; + gl.utils.preventDisabledButtons(); + isClicked = false; + $button = $('#test-button'); + $button.click(function() { + return isClicked = true; + }); + $button.trigger('click'); + return expect(isClicked).toBe(false); + }); + return it('should be on the same page if a disabled link clicked', function() { + var locationBeforeLinkClick; + locationBeforeLinkClick = window.location.href; + gl.utils.preventDisabledButtons(); + $('#test-link').click(); + return expect(window.location.href).toBe(locationBeforeLinkClick); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee deleted file mode 100644 index 4b6a2bb5440..00000000000 --- a/spec/javascripts/application_spec.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -#= require lib/utils/common_utils - -describe 'Application', -> - describe 'disable buttons', -> - fixture.preload('application.html') - - beforeEach -> - fixture.load('application.html') - - it 'should prevent default action for disabled buttons', -> - - gl.utils.preventDisabledButtons() - - isClicked = false - $button = $ '#test-button' - - $button.click -> isClicked = true - $button.trigger 'click' - - expect(isClicked).toBe false - - - it 'should be on the same page if a disabled link clicked', -> - - locationBeforeLinkClick = window.location.href - gl.utils.preventDisabledButtons() - - $('#test-link').click() - - expect(window.location.href).toBe locationBeforeLinkClick diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js new file mode 100644 index 00000000000..3ddc163033e --- /dev/null +++ b/spec/javascripts/awards_handler_spec.js @@ -0,0 +1,187 @@ + +/*= require awards_handler */ + + +/*= require jquery */ + + +/*= require jquery.cookie */ + + +/*= require ./fixtures/emoji_menu */ + +(function() { + var awardsHandler, lazyAssert; + + awardsHandler = null; + + window.gl || (window.gl = {}); + + window.gon || (window.gon = {}); + + gl.emojiAliases = function() { + return { + '+1': 'thumbsup', + '-1': 'thumbsdown' + }; + }; + + gon.award_menu_url = '/emojis'; + + lazyAssert = function(done, assertFn) { + return setTimeout(function() { + assertFn(); + return done(); + }, 333); + }; + + describe('AwardsHandler', function() { + fixture.preload('awards_handler.html'); + beforeEach(function() { + fixture.load('awards_handler.html'); + awardsHandler = new AwardsHandler; + spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { + return function(url, emoji, cb) { + return cb(); + }; + })(this)); + return spyOn(jQuery, 'get').and.callFake(function(req, cb) { + return cb(window.emojiMenu); + }); + }); + describe('::showEmojiMenu', function() { + it('should show emoji menu when Add emoji button clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + expect($emojiMenu.find('#emoji_search').length).toBe(1); + return expect($('.js-awards-block.current').length).toBe(1); + }); + }); + it('should also show emoji menu for the smiley icon in notes', function(done) { + $('.note-action-button').click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + return expect($emojiMenu.length).toBe(1); + }); + }); + return it('should remove emoji menu when body is clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + $('body').click(); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(false); + return expect($('.js-awards-block.current').length).toBe(0); + }); + }); + }); + describe('::addAwardToEmojiBar', function() { + it('should add emoji to votes block', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + expect($emojiButton.length).toBe(1); + expect($emojiButton.next('.js-counter').text()).toBe('1'); + return expect($votesBlock.hasClass('hidden')).toBe(false); + }); + it('should remove the emoji when we click again', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + return expect($emojiButton.length).toBe(0); + }); + return it('should decrement the emoji counter', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + $emojiButton.next('.js-counter').text(5); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + expect($emojiButton.length).toBe(1); + return expect($emojiButton.next('.js-counter').text()).toBe('4'); + }); + }); + describe('::getAwardUrl', function() { + return it('should return the url for request', function() { + return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'); + }); + }); + describe('::addAward and ::checkMutuality', function() { + return it('should handle :+1: and :-1: mutuality', function() { + var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl; + awardUrl = awardsHandler.getAwardUrl(); + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent(); + awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); + expect($thumbsUpEmoji.hasClass('active')).toBe(true); + expect($thumbsDownEmoji.hasClass('active')).toBe(false); + $thumbsUpEmoji.tooltip(); + $thumbsDownEmoji.tooltip(); + awardsHandler.addAward($votesBlock, awardUrl, 'thumbsdown', true); + expect($thumbsUpEmoji.hasClass('active')).toBe(false); + return expect($thumbsDownEmoji.hasClass('active')).toBe(true); + }); + }); + describe('::removeEmoji', function() { + return it('should remove emoji', function() { + var $votesBlock, awardUrl; + awardUrl = awardsHandler.getAwardUrl(); + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAward($votesBlock, awardUrl, 'fire', false); + expect($votesBlock.find('[data-emoji=fire]').length).toBe(1); + awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button')); + return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0); + }); + }); + describe('search', function() { + return it('should filter the emoji', function() { + $('.js-add-award').eq(0).click(); + expect($('[data-emoji=angel]').is(':visible')).toBe(true); + expect($('[data-emoji=anger]').is(':visible')).toBe(true); + $('#emoji_search').val('ali').trigger('keyup'); + expect($('[data-emoji=angel]').is(':visible')).toBe(false); + expect($('[data-emoji=anger]').is(':visible')).toBe(false); + return expect($('[data-emoji=alien]').is(':visible')).toBe(true); + }); + }); + return describe('emoji menu', function() { + var openEmojiMenuAndAddEmoji, selector; + selector = '[data-emoji=sunglasses]'; + openEmojiMenuAndAddEmoji = function() { + var $block, $emoji, $menu; + $('.js-add-award').eq(0).click(); + $menu = $('.emoji-menu'); + $block = $('.js-awards-block'); + $emoji = $menu.find(".emoji-menu-list-item " + selector); + expect($emoji.length).toBe(1); + expect($block.find(selector).length).toBe(0); + $emoji.click(); + expect($menu.hasClass('.is-visible')).toBe(false); + return expect($block.find(selector).length).toBe(1); + }; + it('should add selected emoji to awards block', function() { + return openEmojiMenuAndAddEmoji(); + }); + return it('should remove already selected emoji', function() { + var $block, $emoji; + openEmojiMenuAndAddEmoji(); + $('.js-add-award').eq(0).click(); + $block = $('.js-awards-block'); + $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector); + $emoji.click(); + return expect($block.find(selector).length).toBe(0); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee deleted file mode 100644 index d7f9c6fc076..00000000000 --- a/spec/javascripts/awards_handler_spec.js.coffee +++ /dev/null @@ -1,200 +0,0 @@ -#= require awards_handler -#= require jquery -#= require jquery.cookie -#= require ./fixtures/emoji_menu - -awardsHandler = null -window.gl or= {} -window.gon or= {} -gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } -gon.award_menu_url = '/emojis' - - -lazyAssert = (done, assertFn) -> - - setTimeout -> # Maybe jasmine.clock here? - assertFn() - done() - , 333 - - -describe 'AwardsHandler', -> - - fixture.preload 'awards_handler.html' - - beforeEach -> - fixture.load 'awards_handler.html' - awardsHandler = new AwardsHandler - spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() - spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu - - - describe '::showEmojiMenu', -> - - it 'should show emoji menu when Add emoji button clicked', (done) -> - - $('.js-add-award').eq(0).click() - - lazyAssert done, -> - $emojiMenu = $ '.emoji-menu' - expect($emojiMenu.length).toBe 1 - expect($emojiMenu.hasClass('is-visible')).toBe yes - expect($emojiMenu.find('#emoji_search').length).toBe 1 - expect($('.js-awards-block.current').length).toBe 1 - - - it 'should also show emoji menu for the smiley icon in notes', (done) -> - - $('.note-action-button').click() - - lazyAssert done, -> - $emojiMenu = $ '.emoji-menu' - expect($emojiMenu.length).toBe 1 - - - it 'should remove emoji menu when body is clicked', (done) -> - - $('.js-add-award').eq(0).click() - - lazyAssert done, -> - $emojiMenu = $('.emoji-menu') - $('body').click() - expect($emojiMenu.length).toBe 1 - expect($emojiMenu.hasClass('is-visible')).toBe no - expect($('.js-awards-block.current').length).toBe 0 - - - describe '::addAwardToEmojiBar', -> - - it 'should add emoji to votes block', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - $emojiButton = $votesBlock.find '[data-emoji=heart]' - - expect($emojiButton.length).toBe 1 - expect($emojiButton.next('.js-counter').text()).toBe '1' - expect($votesBlock.hasClass('hidden')).toBe no - - - it 'should remove the emoji when we click again', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - $emojiButton = $votesBlock.find '[data-emoji=heart]' - - expect($emojiButton.length).toBe 0 - - - it 'should decrement the emoji counter', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - $emojiButton = $votesBlock.find '[data-emoji=heart]' - $emojiButton.next('.js-counter').text 5 - - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - expect($emojiButton.length).toBe 1 - expect($emojiButton.next('.js-counter').text()).toBe '4' - - - describe '::getAwardUrl', -> - - it 'should return the url for request', -> - - expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji' - - - describe '::addAward and ::checkMutuality', -> - - it 'should handle :+1: and :-1: mutuality', -> - - awardUrl = awardsHandler.getAwardUrl() - $votesBlock = $('.js-awards-block').eq 0 - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent() - $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent() - - awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no - - expect($thumbsUpEmoji.hasClass('active')).toBe yes - expect($thumbsDownEmoji.hasClass('active')).toBe no - - $thumbsUpEmoji.tooltip() - $thumbsDownEmoji.tooltip() - - awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes - - expect($thumbsUpEmoji.hasClass('active')).toBe no - expect($thumbsDownEmoji.hasClass('active')).toBe yes - - - describe '::removeEmoji', -> - - it 'should remove emoji', -> - - awardUrl = awardsHandler.getAwardUrl() - $votesBlock = $('.js-awards-block').eq 0 - - awardsHandler.addAward $votesBlock, awardUrl, 'fire', no - expect($votesBlock.find('[data-emoji=fire]').length).toBe 1 - - awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button') - expect($votesBlock.find('[data-emoji=fire]').length).toBe 0 - - - describe 'search', -> - - it 'should filter the emoji', -> - - $('.js-add-award').eq(0).click() - - expect($('[data-emoji=angel]').is(':visible')).toBe yes - expect($('[data-emoji=anger]').is(':visible')).toBe yes - - $('#emoji_search').val('ali').trigger 'keyup' - - expect($('[data-emoji=angel]').is(':visible')).toBe no - expect($('[data-emoji=anger]').is(':visible')).toBe no - expect($('[data-emoji=alien]').is(':visible')).toBe yes - - - describe 'emoji menu', -> - - selector = '[data-emoji=sunglasses]' - - openEmojiMenuAndAddEmoji = -> - - $('.js-add-award').eq(0).click() - - $menu = $ '.emoji-menu' - $block = $ '.js-awards-block' - $emoji = $menu.find ".emoji-menu-list-item #{selector}" - - expect($emoji.length).toBe 1 - expect($block.find(selector).length).toBe 0 - - $emoji.click() - - expect($menu.hasClass('.is-visible')).toBe no - expect($block.find(selector).length).toBe 1 - - - it 'should add selected emoji to awards block', -> - - openEmojiMenuAndAddEmoji() - - - it 'should remove already selected emoji', -> - - openEmojiMenuAndAddEmoji() - $('.js-add-award').eq(0).click() - - $block = $ '.js-awards-block' - $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}" - - $emoji.click() - expect($block.find(selector).length).toBe 0 diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js new file mode 100644 index 00000000000..78795f7654a --- /dev/null +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -0,0 +1,21 @@ + +/*= require behaviors/autosize */ + +(function() { + describe('Autosize behavior', function() { + var load; + beforeEach(function() { + return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + }); + it('does not overwrite the resize property', function() { + load(); + return expect($('textarea')).toHaveCss({ + resize: 'vertical' + }); + }); + return load = function() { + return $(document).trigger('page:load'); + }; + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee deleted file mode 100644 index 7fc1d19c35f..00000000000 --- a/spec/javascripts/behaviors/autosize_spec.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -#= require behaviors/autosize - -describe 'Autosize behavior', -> - beforeEach -> - fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>') - - it 'does not overwrite the resize property', -> - load() - expect($('textarea')).toHaveCss(resize: 'vertical') - - load = -> $(document).trigger('page:load') diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js new file mode 100644 index 00000000000..4c52ecd903d --- /dev/null +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -0,0 +1,93 @@ + +/*= require behaviors/quick_submit */ + +(function() { + describe('Quick Submit behavior', function() { + var keydownEvent; + fixture.preload('behaviors/quick_submit.html'); + beforeEach(function() { + fixture.load('behaviors/quick_submit.html'); + $('form').submit(function(e) { + return e.preventDefault(); + }); + return this.spies = { + submit: spyOnEvent('form', 'submit') + }; + }); + it('does not respond to other keyCodes', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + keyCode: 32 + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('does not respond to Enter alone', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + ctrlKey: false, + metaKey: false + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('does not respond to repeated events', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + repeat: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('disables submit buttons', function() { + $('textarea').trigger(keydownEvent()); + expect($('input[type=submit]')).toBeDisabled(); + return expect($('button[type=submit]')).toBeDisabled(); + }); + if (navigator.userAgent.match(/Macintosh/)) { + it('responds to Meta+Enter', function() { + $('input.quick-submit-input').trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + it('excludes other modifier keys', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + altKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + ctrlKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + shiftKey: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } else { + it('responds to Ctrl+Enter', function() { + $('input.quick-submit-input').trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + it('excludes other modifier keys', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + altKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + metaKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + shiftKey: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } + return keydownEvent = function(options) { + var defaults; + if (navigator.userAgent.match(/Macintosh/)) { + defaults = { + keyCode: 13, + metaKey: true + }; + } else { + defaults = { + keyCode: 13, + ctrlKey: true + }; + } + return $.Event('keydown', $.extend({}, defaults, options)); + }; + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee deleted file mode 100644 index d3b003a328a..00000000000 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ /dev/null @@ -1,70 +0,0 @@ -#= require behaviors/quick_submit - -describe 'Quick Submit behavior', -> - fixture.preload('behaviors/quick_submit.html') - - beforeEach -> - fixture.load('behaviors/quick_submit.html') - - # Prevent a form submit from moving us off the testing page - $('form').submit (e) -> e.preventDefault() - - @spies = { - submit: spyOnEvent('form', 'submit') - } - - it 'does not respond to other keyCodes', -> - $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'does not respond to Enter alone', -> - $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'does not respond to repeated events', -> - $('input.quick-submit-input').trigger(keydownEvent(repeat: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'disables submit buttons', -> - $('textarea').trigger(keydownEvent()) - - expect($('input[type=submit]')).toBeDisabled() - expect($('button[type=submit]')).toBeDisabled() - - # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll - # only run the tests that apply to the current platform - if navigator.userAgent.match(/Macintosh/) - it 'responds to Meta+Enter', -> - $('input.quick-submit-input').trigger(keydownEvent()) - - expect(@spies.submit).toHaveBeenTriggered() - - it 'excludes other modifier keys', -> - $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - else - it 'responds to Ctrl+Enter', -> - $('input.quick-submit-input').trigger(keydownEvent()) - - expect(@spies.submit).toHaveBeenTriggered() - - it 'excludes other modifier keys', -> - $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(metaKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - keydownEvent = (options) -> - if navigator.userAgent.match(/Macintosh/) - defaults = { keyCode: 13, metaKey: true } - else - defaults = { keyCode: 13, ctrlKey: true } - - $.Event('keydown', $.extend({}, defaults, options)) diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js new file mode 100644 index 00000000000..724c3baf989 --- /dev/null +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -0,0 +1,44 @@ + +/*= require behaviors/requires_input */ + +(function() { + describe('requiresInput', function() { + fixture.preload('behaviors/requires_input.html'); + beforeEach(function() { + return fixture.load('behaviors/requires_input.html'); + }); + it('disables submit when any field is required', function() { + $('.js-requires-input').requiresInput(); + return expect($('.submit')).toBeDisabled(); + }); + it('enables submit when no field is required', function() { + $('*[required=required]').removeAttr('required'); + $('.js-requires-input').requiresInput(); + return expect($('.submit')).not.toBeDisabled(); + }); + it('enables submit when all required fields are pre-filled', function() { + $('*[required=required]').remove(); + $('.js-requires-input').requiresInput(); + return expect($('.submit')).not.toBeDisabled(); + }); + it('enables submit when all required fields receive input', function() { + $('.js-requires-input').requiresInput(); + $('#required1').val('input1').change(); + expect($('.submit')).toBeDisabled(); + $('#optional1').val('input1').change(); + expect($('.submit')).toBeDisabled(); + $('#required2').val('input2').change(); + $('#required3').val('input3').change(); + $('#required4').val('input4').change(); + $('#required5').val('1').change(); + return expect($('.submit')).not.toBeDisabled(); + }); + return it('is called on page:load event', function() { + var spy; + spy = spyOn($.fn, 'requiresInput'); + $(document).trigger('page:load'); + return expect(spy).toHaveBeenCalled(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/requires_input_spec.js.coffee b/spec/javascripts/behaviors/requires_input_spec.js.coffee deleted file mode 100644 index 61a17632173..00000000000 --- a/spec/javascripts/behaviors/requires_input_spec.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -#= require behaviors/requires_input - -describe 'requiresInput', -> - fixture.preload('behaviors/requires_input.html') - - beforeEach -> - fixture.load('behaviors/requires_input.html') - - it 'disables submit when any field is required', -> - $('.js-requires-input').requiresInput() - - expect($('.submit')).toBeDisabled() - - it 'enables submit when no field is required', -> - $('*[required=required]').removeAttr('required') - - $('.js-requires-input').requiresInput() - - expect($('.submit')).not.toBeDisabled() - - it 'enables submit when all required fields are pre-filled', -> - $('*[required=required]').remove() - - $('.js-requires-input').requiresInput() - - expect($('.submit')).not.toBeDisabled() - - it 'enables submit when all required fields receive input', -> - $('.js-requires-input').requiresInput() - - $('#required1').val('input1').change() - expect($('.submit')).toBeDisabled() - - $('#optional1').val('input1').change() - expect($('.submit')).toBeDisabled() - - $('#required2').val('input2').change() - $('#required3').val('input3').change() - $('#required4').val('input4').change() - $('#required5').val('1').change() - - expect($('.submit')).not.toBeDisabled() - - it 'is called on page:load event', -> - spy = spyOn($.fn, 'requiresInput') - - $(document).trigger('page:load') - - expect(spy).toHaveBeenCalled() diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js new file mode 100644 index 00000000000..eced2f6575d --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js @@ -0,0 +1,22 @@ + +/*= require extensions/array */ + +(function() { + describe('Array extensions', function() { + describe('first', function() { + return it('returns the first item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.first()).toBe(0); + }); + }); + return describe('last', function() { + return it('returns the last item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.last()).toBe(5); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee deleted file mode 100644 index 4ceac619422..00000000000 --- a/spec/javascripts/extensions/array_spec.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require extensions/array - -describe 'Array extensions', -> - describe 'first', -> - it 'returns the first item', -> - arr = [0, 1, 2, 3, 4, 5] - expect(arr.first()).toBe(0) - - describe 'last', -> - it 'returns the last item', -> - arr = [0, 1, 2, 3, 4, 5] - expect(arr.last()).toBe(5) diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js new file mode 100644 index 00000000000..b644344b95a --- /dev/null +++ b/spec/javascripts/extensions/jquery_spec.js @@ -0,0 +1,42 @@ + +/*= require extensions/jquery */ + +(function() { + describe('jQuery extensions', function() { + describe('disable', function() { + beforeEach(function() { + return fixture.set('<input type="text" />'); + }); + it('adds the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveAttr('disabled', 'disabled'); + }); + return it('adds the disabled class', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveClass('disabled'); + }); + }); + return describe('enable', function() { + beforeEach(function() { + return fixture.set('<input type="text" disabled="disabled" class="disabled" />'); + }); + it('removes the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveAttr('disabled'); + }); + return it('removes the disabled class', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveClass('disabled'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee deleted file mode 100644 index b10e16b7d01..00000000000 --- a/spec/javascripts/extensions/jquery_spec.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -#= require extensions/jquery - -describe 'jQuery extensions', -> - describe 'disable', -> - beforeEach -> - fixture.set '<input type="text" />' - - it 'adds the disabled attribute', -> - $input = $('input').first() - - $input.disable() - expect($input).toHaveAttr('disabled', 'disabled') - - it 'adds the disabled class', -> - $input = $('input').first() - - $input.disable() - expect($input).toHaveClass('disabled') - - describe 'enable', -> - beforeEach -> - fixture.set '<input type="text" disabled="disabled" class="disabled" />' - - it 'removes the disabled attribute', -> - $input = $('input').first() - - $input.enable() - expect($input).not.toHaveAttr('disabled') - - it 'removes the disabled class', -> - $input = $('input').first() - - $input.enable() - expect($input).not.toHaveClass('disabled') diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee deleted file mode 100644 index ce1a41390d2..00000000000 --- a/spec/javascripts/fixtures/emoji_menu.coffee +++ /dev/null @@ -1,957 +0,0 @@ -window.emojiMenu = """ - <div class='emoji-menu'> - <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" /> - <div class='emoji-menu-content'> - <h5 class='emoji-menu-title'> - Emoticons - </h5> - <ul class='clearfix emoji-menu-list'> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F472" title="man_with_gua_pi_mao" data-aliases="" data-emoji="man_with_gua_pi_mao" data-unicode-name="1F472"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F471" title="person_with_blond_hair" data-aliases="" data-emoji="person_with_blond_hair" data-unicode-name="1F471"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64E" title="person_with_pouting_face" data-aliases="" data-emoji="person_with_pouting_face" data-unicode-name="1F64E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3BD" title="running_shirt_with_sash" data-aliases="" data-emoji="running_shirt_with_sash" data-unicode-name="1F3BD"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61D" title="stuck_out_tongue_closed_eyes" data-aliases="" data-emoji="stuck_out_tongue_closed_eyes" data-unicode-name="1F61D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61C" title="stuck_out_tongue_winking_eye" data-aliases="" data-emoji="stuck_out_tongue_winking_eye" data-unicode-name="1F61C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46C" title="two_men_holding_hands" data-aliases="" data-emoji="two_men_holding_hands" data-unicode-name="1F46C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46D" title="two_women_holding_hands" data-aliases="" data-emoji="two_women_holding_hands" data-unicode-name="1F46D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div> - </button> - </li> - </ul> - </div> - </div> -""" diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js new file mode 100644 index 00000000000..99e3f7247bd --- /dev/null +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -0,0 +1,4 @@ +(function() { + window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; + +}).call(this); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js new file mode 100644 index 00000000000..dc6231ebb38 --- /dev/null +++ b/spec/javascripts/issue_spec.js @@ -0,0 +1,121 @@ + +/*= require lib/utils/text_utility */ + + +/*= require issue */ + +(function() { + describe('Issue', function() { + return describe('task lists', function() { + fixture.preload('issues_show.html'); + beforeEach(function() { + fixture.load('issues_show.html'); + return this.issue = new Issue(); + }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('/foo'); + return expect(req.data.issue.description).not.toBe(null); + }); + return $('.js-task-list-field').trigger('tasklist:changed'); + }); + }); + }); + + describe('reopen/close issue', function() { + fixture.preload('issues_show.html'); + beforeEach(function() { + fixture.load('issues_show.html'); + return this.issue = new Issue(); + }); + it('closes an issue', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://gitlab.com/issues/6/close'); + return req.success({ + id: 34 + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeVisible(); + expect($btnClose).toBeHidden(); + expect($('div.status-box-closed')).toBeVisible(); + return expect($('div.status-box-open')).toBeHidden(); + }); + it('fails to close an issue with success:false', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://goesnowhere.nothing/whereami'); + return req.success({ + saved: false + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-closed')).toBeHidden(); + expect($('div.status-box-open')).toBeVisible(); + expect($('div.flash-alert')).toBeVisible(); + return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + }); + it('fails to closes an issue with HTTP error', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://goesnowhere.nothing/whereami'); + return req.error(); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-closed')).toBeHidden(); + expect($('div.status-box-open')).toBeVisible(); + expect($('div.flash-alert')).toBeVisible(); + return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + }); + return it('reopens an issue', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://gitlab.com/issues/6/reopen'); + return req.success({ + id: 34 + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + expect($btnReopen.text()).toBe('Reopen'); + $btnReopen.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-open')).toBeVisible(); + return expect($('div.status-box-closed')).toBeHidden(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee deleted file mode 100644 index d84d80f266b..00000000000 --- a/spec/javascripts/issue_spec.js.coffee +++ /dev/null @@ -1,109 +0,0 @@ -#= require lib/utils/text_utility -#= require issue - -describe 'Issue', -> - describe 'task lists', -> - fixture.preload('issues_show.html') - - beforeEach -> - fixture.load('issues_show.html') - @issue = new Issue() - - it 'modifies the Markdown field', -> - spyOn(jQuery, 'ajax').and.stub() - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits an ajax request on tasklist:changed', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PATCH') - expect(req.url).toBe('/foo') - expect(req.data.issue.description).not.toBe(null) - - $('.js-task-list-field').trigger('tasklist:changed') -describe 'reopen/close issue', -> - fixture.preload('issues_show.html') - beforeEach -> - fixture.load('issues_show.html') - @issue = new Issue() - it 'closes an issue', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://gitlab.com/issues/6/close') - req.success id: 34 - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeVisible() - expect($btnClose).toBeHidden() - expect($('div.status-box-closed')).toBeVisible() - expect($('div.status-box-open')).toBeHidden() - - it 'fails to close an issue with success:false', -> - - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://goesnowhere.nothing/whereami') - req.success saved: false - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - $btnClose.attr('href','http://goesnowhere.nothing/whereami') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() - expect($('div.status-box-open')).toBeVisible() - expect($('div.flash-alert')).toBeVisible() - expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') - - it 'fails to closes an issue with HTTP error', -> - - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://goesnowhere.nothing/whereami') - req.error() - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - $btnClose.attr('href','http://goesnowhere.nothing/whereami') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() - expect($('div.status-box-open')).toBeVisible() - expect($('div.flash-alert')).toBeVisible() - expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') - - it 'reopens an issue', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://gitlab.com/issues/6/reopen') - req.success id: 34 - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - expect($btnReopen.text()).toBe('Reopen') - - $btnReopen.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-open')).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js new file mode 100644 index 00000000000..e2789571607 --- /dev/null +++ b/spec/javascripts/line_highlighter_spec.js @@ -0,0 +1,229 @@ + +/*= require line_highlighter */ + +(function() { + describe('LineHighlighter', function() { + var clickLine; + fixture.preload('line_highlighter.html'); + clickLine = function(number, eventData) { + var e; + if (eventData == null) { + eventData = {}; + } + if ($.isEmptyObject(eventData)) { + return $("#L" + number).mousedown().click(); + } else { + e = $.Event('mousedown', eventData); + return $("#L" + number).trigger(e).click(); + } + }; + beforeEach(function() { + fixture.load('line_highlighter.html'); + this["class"] = new LineHighlighter(); + this.css = this["class"].highlightClass; + return this.spies = { + __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {}) + }; + }); + describe('behavior', function() { + it('highlights one line given in the URL hash', function() { + new LineHighlighter('#L13'); + return expect($('#LC13')).toHaveClass(this.css); + }); + it('highlights a range of lines given in the URL hash', function() { + var i, line, results; + new LineHighlighter('#L5-25'); + expect($("." + this.css).length).toBe(21); + results = []; + for (line = i = 5; i <= 25; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + it('scrolls to the first highlighted line on initial load', function() { + var spy; + spy = spyOn($, 'scrollTo'); + new LineHighlighter('#L5-25'); + return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); + }); + it('discards click events', function() { + var spy; + spy = spyOnEvent('a[data-line-number]', 'click'); + clickLine(13); + return expect(spy).toHaveBeenPrevented(); + }); + return it('handles garbage input from the hash', function() { + var func; + func = function() { + return new LineHighlighter('#blob-content-holder'); + }; + return expect(func).not.toThrow(); + }); + }); + describe('#clickHandler', function() { + it('discards the mousedown event', function() { + var spy; + spy = spyOnEvent('a[data-line-number]', 'mousedown'); + clickLine(13); + return expect(spy).toHaveBeenPrevented(); + }); + it('handles clicking on a child icon element', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + $('#L13 i').mousedown().click(); + expect(spy).toHaveBeenCalledWith(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + describe('without shiftKey', function() { + it('highlights one line when clicked', function() { + clickLine(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + it('unhighlights previously highlighted lines', function() { + clickLine(13); + clickLine(20); + expect($('#LC13')).not.toHaveClass(this.css); + return expect($('#LC20')).toHaveClass(this.css); + }); + return it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + clickLine(13); + return expect(spy).toHaveBeenCalledWith(13); + }); + }); + return describe('with shiftKey', function() { + it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + clickLine(13); + clickLine(20, { + shiftKey: true + }); + expect(spy).toHaveBeenCalledWith(13); + return expect(spy).toHaveBeenCalledWith(13, 20); + }); + describe('without existing highlight', function() { + it('highlights the clicked line', function() { + clickLine(13, { + shiftKey: true + }); + expect($('#LC13')).toHaveClass(this.css); + return expect($("." + this.css).length).toBe(1); + }); + return it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash'); + clickLine(13, { + shiftKey: true + }); + return expect(spy).toHaveBeenCalledWith(13); + }); + }); + describe('with existing single-line highlight', function() { + it('uses existing line as last line when target is lesser', function() { + var i, line, results; + clickLine(20); + clickLine(15, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 15; i <= 20; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + return it('uses existing line as first line when target is greater', function() { + var i, line, results; + clickLine(5); + clickLine(10, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 5; i <= 10; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + }); + return describe('with existing multi-line highlight', function() { + beforeEach(function() { + clickLine(10, { + shiftKey: true + }); + return clickLine(13, { + shiftKey: true + }); + }); + it('uses target as first line when it is less than existing first line', function() { + var i, line, results; + clickLine(5, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 5; i <= 10; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + return it('uses target as last line when it is greater than existing first line', function() { + var i, line, results; + clickLine(15, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 10; i <= 15; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + }); + }); + }); + describe('#hashToRange', function() { + beforeEach(function() { + return this.subject = this["class"].hashToRange; + }); + it('extracts a single line number from the hash', function() { + return expect(this.subject('#L5')).toEqual([5, null]); + }); + it('extracts a range of line numbers from the hash', function() { + return expect(this.subject('#L5-15')).toEqual([5, 15]); + }); + return it('returns [null, null] when the hash is not a line number', function() { + return expect(this.subject('#foo')).toEqual([null, null]); + }); + }); + describe('#highlightLine', function() { + beforeEach(function() { + return this.subject = this["class"].highlightLine; + }); + it('highlights the specified line', function() { + this.subject(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + return it('accepts a String-based number', function() { + this.subject('13'); + return expect($('#LC13')).toHaveClass(this.css); + }); + }); + return describe('#setHash', function() { + beforeEach(function() { + return this.subject = this["class"].setHash; + }); + it('sets the location hash for a single line', function() { + this.subject(5); + return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5'); + }); + return it('sets the location hash for a range', function() { + this.subject(5, 15); + return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee deleted file mode 100644 index a073f21e7bc..00000000000 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ /dev/null @@ -1,158 +0,0 @@ -#= require line_highlighter - -describe 'LineHighlighter', -> - fixture.preload('line_highlighter.html') - - clickLine = (number, eventData = {}) -> - if $.isEmptyObject(eventData) - $("#L#{number}").mousedown().click() - else - e = $.Event 'mousedown', eventData - $("#L#{number}").trigger(e).click() - - beforeEach -> - fixture.load('line_highlighter.html') - @class = new LineHighlighter() - @css = @class.highlightClass - @spies = { - __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> - } - - describe 'behavior', -> - it 'highlights one line given in the URL hash', -> - new LineHighlighter('#L13') - expect($('#LC13')).toHaveClass(@css) - - it 'highlights a range of lines given in the URL hash', -> - new LineHighlighter('#L5-25') - expect($(".#{@css}").length).toBe(21) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25] - - it 'scrolls to the first highlighted line on initial load', -> - spy = spyOn($, 'scrollTo') - new LineHighlighter('#L5-25') - expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) - - it 'discards click events', -> - spy = spyOnEvent('a[data-line-number]', 'click') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles garbage input from the hash', -> - func = -> new LineHighlighter('#blob-content-holder') - expect(func).not.toThrow() - - describe '#clickHandler', -> - it 'discards the mousedown event', -> - spy = spyOnEvent('a[data-line-number]', 'mousedown') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles clicking on a child icon element', -> - spy = spyOn(@class, 'setHash').and.callThrough() - - $('#L13 i').mousedown().click() - - expect(spy).toHaveBeenCalledWith(13) - expect($('#LC13')).toHaveClass(@css) - - describe 'without shiftKey', -> - it 'highlights one line when clicked', -> - clickLine(13) - expect($('#LC13')).toHaveClass(@css) - - it 'unhighlights previously highlighted lines', -> - clickLine(13) - clickLine(20) - - expect($('#LC13')).not.toHaveClass(@css) - expect($('#LC20')).toHaveClass(@css) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with shiftKey', -> - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - clickLine(20, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - expect(spy).toHaveBeenCalledWith(13, 20) - - describe 'without existing highlight', -> - it 'highlights the clicked line', -> - clickLine(13, shiftKey: true) - expect($('#LC13')).toHaveClass(@css) - expect($(".#{@css}").length).toBe(1) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash') - clickLine(13, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with existing single-line highlight', -> - it 'uses existing line as last line when target is lesser', -> - clickLine(20) - clickLine(15, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20] - - it 'uses existing line as first line when target is greater', -> - clickLine(5) - clickLine(10, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] - - describe 'with existing multi-line highlight', -> - beforeEach -> - clickLine(10, shiftKey: true) - clickLine(13, shiftKey: true) - - it 'uses target as first line when it is less than existing first line', -> - clickLine(5, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] - - it 'uses target as last line when it is greater than existing first line', -> - clickLine(15, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15] - - describe '#hashToRange', -> - beforeEach -> - @subject = @class.hashToRange - - it 'extracts a single line number from the hash', -> - expect(@subject('#L5')).toEqual([5, null]) - - it 'extracts a range of line numbers from the hash', -> - expect(@subject('#L5-15')).toEqual([5, 15]) - - it 'returns [null, null] when the hash is not a line number', -> - expect(@subject('#foo')).toEqual([null, null]) - - describe '#highlightLine', -> - beforeEach -> - @subject = @class.highlightLine - - it 'highlights the specified line', -> - @subject(13) - expect($('#LC13')).toHaveClass(@css) - - it 'accepts a String-based number', -> - @subject('13') - expect($('#LC13')).toHaveClass(@css) - - describe '#setHash', -> - beforeEach -> - @subject = @class.setHash - - it 'sets the location hash for a single line', -> - @subject(5) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') - - it 'sets the location hash for a range', -> - @subject(5, 15) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js new file mode 100644 index 00000000000..61830d267a9 --- /dev/null +++ b/spec/javascripts/merge_request_spec.js @@ -0,0 +1,28 @@ + +/*= require merge_request */ + +(function() { + describe('MergeRequest', function() { + return describe('task lists', function() { + fixture.preload('merge_requests_show.html'); + beforeEach(function() { + fixture.load('merge_requests_show.html'); + return this.merge = new MergeRequest(); + }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('/foo'); + return expect(req.data.merge_request.description).not.toBe(null); + }); + return $('.js-task-list-field').trigger('tasklist:changed'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee deleted file mode 100644 index 3cb67d51c85..00000000000 --- a/spec/javascripts/merge_request_spec.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require merge_request - -describe 'MergeRequest', -> - describe 'task lists', -> - fixture.preload('merge_requests_show.html') - - beforeEach -> - fixture.load('merge_requests_show.html') - @merge = new MergeRequest() - - it 'modifies the Markdown field', -> - spyOn(jQuery, 'ajax').and.stub() - - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits an ajax request on tasklist:changed', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PATCH') - expect(req.url).toBe('/foo') - expect(req.data.merge_request.description).not.toBe(null) - - $('.js-task-list-field').trigger('tasklist:changed') diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js new file mode 100644 index 00000000000..395032a7416 --- /dev/null +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -0,0 +1,106 @@ + +/*= require merge_request_tabs */ + +(function() { + describe('MergeRequestTabs', function() { + var stubLocation; + stubLocation = function(stubs) { + var defaults; + defaults = { + pathname: '', + search: '', + hash: '' + }; + return $.extend(defaults, stubs); + }; + fixture.preload('merge_request_tabs.html'); + beforeEach(function() { + this["class"] = new MergeRequestTabs(); + return this.spies = { + ajax: spyOn($, 'ajax').and.callFake(function() {}), + history: spyOn(history, 'replaceState').and.callFake(function() {}) + }; + }); + describe('#activateTab', function() { + beforeEach(function() { + fixture.load('merge_request_tabs.html'); + return this.subject = this["class"].activateTab; + }); + it('shows the first tab when action is show', function() { + this.subject('show'); + return expect($('#notes')).toHaveClass('active'); + }); + it('shows the notes tab when action is notes', function() { + this.subject('notes'); + return expect($('#notes')).toHaveClass('active'); + }); + it('shows the commits tab when action is commits', function() { + this.subject('commits'); + return expect($('#commits')).toHaveClass('active'); + }); + return it('shows the diffs tab when action is diffs', function() { + this.subject('diffs'); + return expect($('#diffs')).toHaveClass('active'); + }); + }); + return describe('#setCurrentAction', function() { + beforeEach(function() { + return this.subject = this["class"].setCurrentAction; + }); + it('changes from commits', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/commits' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + }); + it('changes from diffs', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('changes from diffs.html', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs.html' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('changes from notes', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1' + }); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('includes search parameters and hash string', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', + search: '?view=parallel', + hash: '#L15-35' + }); + return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + }); + it('replaces the current history state', function() { + var new_state; + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1' + }); + new_state = this.subject('commits'); + return expect(this.spies.history).toHaveBeenCalledWith({ + turbolinks: true, + url: new_state + }, document.title, new_state); + }); + return it('treats "show" like "notes"', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/commits' + }); + return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee deleted file mode 100644 index a0cfba455ea..00000000000 --- a/spec/javascripts/merge_request_tabs_spec.js.coffee +++ /dev/null @@ -1,88 +0,0 @@ -#= require merge_request_tabs - -describe 'MergeRequestTabs', -> - stubLocation = (stubs) -> - defaults = {pathname: '', search: '', hash: ''} - $.extend(defaults, stubs) - - fixture.preload('merge_request_tabs.html') - - beforeEach -> - @class = new MergeRequestTabs() - @spies = { - ajax: spyOn($, 'ajax').and.callFake -> - history: spyOn(history, 'replaceState').and.callFake -> - } - - describe '#activateTab', -> - beforeEach -> - fixture.load('merge_request_tabs.html') - @subject = @class.activateTab - - it 'shows the first tab when action is show', -> - @subject('show') - expect($('#notes')).toHaveClass('active') - - it 'shows the notes tab when action is notes', -> - @subject('notes') - expect($('#notes')).toHaveClass('active') - - it 'shows the commits tab when action is commits', -> - @subject('commits') - expect($('#commits')).toHaveClass('active') - - it 'shows the diffs tab when action is diffs', -> - @subject('diffs') - expect($('#diffs')).toHaveClass('active') - - describe '#setCurrentAction', -> - beforeEach -> - @subject = @class.setCurrentAction - - it 'changes from commits', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') - - it 'changes from diffs', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'changes from diffs.html', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'changes from notes', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') - - expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'includes search parameters and hash string', -> - @class._location = stubLocation({ - pathname: '/foo/bar/merge_requests/1/diffs' - search: '?view=parallel' - hash: '#L15-35' - }) - - expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35') - - it 'replaces the current history state', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') - new_state = @subject('commits') - - expect(@spies.history).toHaveBeenCalledWith( - {turbolinks: true, url: new_state}, - document.title, - new_state - ) - - it 'treats "show" like "notes"', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') - - expect(@subject('show')).toBe('/foo/bar/merge_requests/1') diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js new file mode 100644 index 00000000000..17b32914ec3 --- /dev/null +++ b/spec/javascripts/merge_request_widget_spec.js @@ -0,0 +1,74 @@ + +/*= require merge_request_widget */ + +(function() { + describe('MergeRequestWidget', function() { + beforeEach(function() { + window.notifyPermissions = function() {}; + window.notify = function() {}; + this.opts = { + ci_status_url: "http://sampledomain.local/ci/getstatus", + ci_status: "", + ci_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, + gitlab_icon: "gitlab_logo.png", + builds_path: "http://sampledomain.local/sampleBuildsPath" + }; + this["class"] = new MergeRequestWidget(this.opts); + return this.ciStatusData = { + "title": "Sample MR title", + "sha": "12a34bc5", + "status": "success", + "coverage": 98 + }; + }); + return describe('getCIStatus', function() { + beforeEach(function() { + return spyOn(jQuery, 'getJSON').and.callFake((function(_this) { + return function(req, cb) { + return cb(_this.ciStatusData); + }; + })(this)); + }); + it('should call showCIStatus even if a notification should not be displayed', function() { + var spy; + spy = spyOn(this["class"], 'showCIStatus').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); + }); + it('should call showCIStatus when a notification should be displayed', function() { + var spy; + spy = spyOn(this["class"], 'showCIStatus').and.stub(); + this["class"].getCIStatus(true); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); + }); + it('should call showCICoverage when the coverage rate is set', function() { + var spy; + spy = spyOn(this["class"], 'showCICoverage').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); + }); + it('should not call showCICoverage when the coverage rate is not set', function() { + var spy; + this.ciStatusData.coverage = null; + spy = spyOn(this["class"], 'showCICoverage').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).not.toHaveBeenCalled(); + }); + return it('should not display a notification on the first check after the widget has been created', function() { + var spy; + spy = spyOn(window, 'notify'); + this["class"] = new MergeRequestWidget(this.opts); + this["class"].getCIStatus(true); + return expect(spy).not.toHaveBeenCalled(); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_widget_spec.js.coffee b/spec/javascripts/merge_request_widget_spec.js.coffee deleted file mode 100644 index 92b7eeb1116..00000000000 --- a/spec/javascripts/merge_request_widget_spec.js.coffee +++ /dev/null @@ -1,55 +0,0 @@ -#= require merge_request_widget - -describe 'MergeRequestWidget', -> - - beforeEach -> - window.notifyPermissions = () -> - window.notify = () -> - @opts = { - ci_status_url:"http://sampledomain.local/ci/getstatus", - ci_status:"", - ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" - }, - ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" - }, - gitlab_icon:"gitlab_logo.png", - builds_path:"http://sampledomain.local/sampleBuildsPath" - } - @class = new MergeRequestWidget(@opts) - @ciStatusData = {"title":"Sample MR title","sha":"12a34bc5","status":"success","coverage":98} - - describe 'getCIStatus', -> - beforeEach -> - spyOn(jQuery, 'getJSON').and.callFake (req, cb) => - cb(@ciStatusData) - - it 'should call showCIStatus even if a notification should not be displayed', -> - spy = spyOn(@class, 'showCIStatus').and.stub() - @class.getCIStatus(false) - expect(spy).toHaveBeenCalledWith(@ciStatusData.status) - - it 'should call showCIStatus when a notification should be displayed', -> - spy = spyOn(@class, 'showCIStatus').and.stub() - @class.getCIStatus(true) - expect(spy).toHaveBeenCalledWith(@ciStatusData.status) - - it 'should call showCICoverage when the coverage rate is set', -> - spy = spyOn(@class, 'showCICoverage').and.stub() - @class.getCIStatus(false) - expect(spy).toHaveBeenCalledWith(@ciStatusData.coverage) - - it 'should not call showCICoverage when the coverage rate is not set', -> - @ciStatusData.coverage = null - spy = spyOn(@class, 'showCICoverage').and.stub() - @class.getCIStatus(false) - expect(spy).not.toHaveBeenCalled() - - it 'should not display a notification on the first check after the widget has been created', -> - spy = spyOn(window, 'notify') - @class = new MergeRequestWidget(@opts) - @class.getCIStatus(true) - expect(spy).not.toHaveBeenCalled() diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js new file mode 100644 index 00000000000..25d3f5b6c04 --- /dev/null +++ b/spec/javascripts/new_branch_spec.js @@ -0,0 +1,170 @@ + +/*= require jquery-ui/autocomplete */ + + +/*= require new_branch_form */ + +(function() { + describe('Branch', function() { + return describe('create a new branch', function() { + var expectToHaveError, fillNameWith; + fixture.preload('new_branch.html'); + fillNameWith = function(value) { + return $('.js-branch-name').val(value).trigger('blur'); + }; + expectToHaveError = function(error) { + return expect($('.js-branch-name-error span').text()).toEqual(error); + }; + beforeEach(function() { + fixture.load('new_branch.html'); + $('form').on('submit', function(e) { + return e.preventDefault(); + }); + return this.form = new NewBranchForm($('.js-create-branch-form'), []); + }); + it("can't start with a dot", function() { + fillNameWith('.foo'); + return expectToHaveError("can't start with '.'"); + }); + it("can't start with a slash", function() { + fillNameWith('/foo'); + return expectToHaveError("can't start with '/'"); + }); + it("can't have two consecutive dots", function() { + fillNameWith('foo..bar'); + return expectToHaveError("can't contain '..'"); + }); + it("can't have spaces anywhere", function() { + fillNameWith(' foo'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo bar'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo '); + return expectToHaveError("can't contain spaces"); + }); + it("can't have ~ anywhere", function() { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + return expectToHaveError("can't contain '~'"); + }); + it("can't have tilde anwhere", function() { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + return expectToHaveError("can't contain '~'"); + }); + it("can't have caret anywhere", function() { + fillNameWith('^foo'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^bar'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^'); + return expectToHaveError("can't contain '^'"); + }); + it("can't have : anywhere", function() { + fillNameWith(':foo'); + expectToHaveError("can't contain ':'"); + fillNameWith('foo:bar'); + expectToHaveError("can't contain ':'"); + fillNameWith(':foo'); + return expectToHaveError("can't contain ':'"); + }); + it("can't have question mark anywhere", function() { + fillNameWith('?foo'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?bar'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?'); + return expectToHaveError("can't contain '?'"); + }); + it("can't have asterisk anywhere", function() { + fillNameWith('*foo'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*bar'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*'); + return expectToHaveError("can't contain '*'"); + }); + it("can't have open bracket anywhere", function() { + fillNameWith('[foo'); + expectToHaveError("can't contain '['"); + fillNameWith('foo[bar'); + expectToHaveError("can't contain '['"); + fillNameWith('foo['); + return expectToHaveError("can't contain '['"); + }); + it("can't have a backslash anywhere", function() { + fillNameWith('\\foo'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\bar'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\'); + return expectToHaveError("can't contain '\\'"); + }); + it("can't contain a sequence @{ anywhere", function() { + fillNameWith('@{foo'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{bar'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{'); + return expectToHaveError("can't contain '@{'"); + }); + it("can't have consecutive slashes", function() { + fillNameWith('foo//bar'); + return expectToHaveError("can't contain consecutive slashes"); + }); + it("can't end with a slash", function() { + fillNameWith('foo/'); + return expectToHaveError("can't end in '/'"); + }); + it("can't end with a dot", function() { + fillNameWith('foo.'); + return expectToHaveError("can't end in '.'"); + }); + it("can't end with .lock", function() { + fillNameWith('foo.lock'); + return expectToHaveError("can't end in '.lock'"); + }); + it("can't be the single character @", function() { + fillNameWith('@'); + return expectToHaveError("can't be '@'"); + }); + it("concatenates all error messages", function() { + fillNameWith('/foo bar?~.'); + return expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'"); + }); + it("doesn't duplicate error messages", function() { + fillNameWith('?foo?bar?zoo?'); + return expectToHaveError("can't contain '?'"); + }); + it("removes the error message when is a valid name", function() { + fillNameWith('foo?bar'); + expect($('.js-branch-name-error span').length).toEqual(1); + fillNameWith('foobar'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have dashes anywhere", function() { + fillNameWith('-foo-bar-zoo-'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have underscores anywhere", function() { + fillNameWith('_foo_bar_zoo_'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have numbers anywhere", function() { + fillNameWith('1foo2bar3zoo4'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + return it("can be only letters", function() { + fillNameWith('foo'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee deleted file mode 100644 index ce773793817..00000000000 --- a/spec/javascripts/new_branch_spec.js.coffee +++ /dev/null @@ -1,160 +0,0 @@ -#= require jquery-ui/autocomplete -#= require new_branch_form - -describe 'Branch', -> - describe 'create a new branch', -> - fixture.preload('new_branch.html') - - fillNameWith = (value) -> - $('.js-branch-name').val(value).trigger('blur') - - expectToHaveError = (error) -> - expect($('.js-branch-name-error span').text()).toEqual(error) - - beforeEach -> - fixture.load('new_branch.html') - $('form').on 'submit', (e) -> e.preventDefault() - - @form = new NewBranchForm($('.js-create-branch-form'), []) - - it "can't start with a dot", -> - fillNameWith '.foo' - expectToHaveError "can't start with '.'" - - it "can't start with a slash", -> - fillNameWith '/foo' - expectToHaveError "can't start with '/'" - - it "can't have two consecutive dots", -> - fillNameWith 'foo..bar' - expectToHaveError "can't contain '..'" - - it "can't have spaces anywhere", -> - fillNameWith ' foo' - expectToHaveError "can't contain spaces" - fillNameWith 'foo bar' - expectToHaveError "can't contain spaces" - fillNameWith 'foo ' - expectToHaveError "can't contain spaces" - - it "can't have ~ anywhere", -> - fillNameWith '~foo' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~bar' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~' - expectToHaveError "can't contain '~'" - - it "can't have tilde anwhere", -> - fillNameWith '~foo' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~bar' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~' - expectToHaveError "can't contain '~'" - - it "can't have caret anywhere", -> - fillNameWith '^foo' - expectToHaveError "can't contain '^'" - fillNameWith 'foo^bar' - expectToHaveError "can't contain '^'" - fillNameWith 'foo^' - expectToHaveError "can't contain '^'" - - it "can't have : anywhere", -> - fillNameWith ':foo' - expectToHaveError "can't contain ':'" - fillNameWith 'foo:bar' - expectToHaveError "can't contain ':'" - fillNameWith ':foo' - expectToHaveError "can't contain ':'" - - it "can't have question mark anywhere", -> - fillNameWith '?foo' - expectToHaveError "can't contain '?'" - fillNameWith 'foo?bar' - expectToHaveError "can't contain '?'" - fillNameWith 'foo?' - expectToHaveError "can't contain '?'" - - it "can't have asterisk anywhere", -> - fillNameWith '*foo' - expectToHaveError "can't contain '*'" - fillNameWith 'foo*bar' - expectToHaveError "can't contain '*'" - fillNameWith 'foo*' - expectToHaveError "can't contain '*'" - - it "can't have open bracket anywhere", -> - fillNameWith '[foo' - expectToHaveError "can't contain '['" - fillNameWith 'foo[bar' - expectToHaveError "can't contain '['" - fillNameWith 'foo[' - expectToHaveError "can't contain '['" - - it "can't have a backslash anywhere", -> - fillNameWith '\\foo' - expectToHaveError "can't contain '\\'" - fillNameWith 'foo\\bar' - expectToHaveError "can't contain '\\'" - fillNameWith 'foo\\' - expectToHaveError "can't contain '\\'" - - it "can't contain a sequence @{ anywhere", -> - fillNameWith '@{foo' - expectToHaveError "can't contain '@{'" - fillNameWith 'foo@{bar' - expectToHaveError "can't contain '@{'" - fillNameWith 'foo@{' - expectToHaveError "can't contain '@{'" - - it "can't have consecutive slashes", -> - fillNameWith 'foo//bar' - expectToHaveError "can't contain consecutive slashes" - - it "can't end with a slash", -> - fillNameWith 'foo/' - expectToHaveError "can't end in '/'" - - it "can't end with a dot", -> - fillNameWith 'foo.' - expectToHaveError "can't end in '.'" - - it "can't end with .lock", -> - fillNameWith 'foo.lock' - expectToHaveError "can't end in '.lock'" - - it "can't be the single character @", -> - fillNameWith '@' - expectToHaveError "can't be '@'" - - it "concatenates all error messages", -> - fillNameWith '/foo bar?~.' - expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'" - - it "doesn't duplicate error messages", -> - fillNameWith '?foo?bar?zoo?' - expectToHaveError "can't contain '?'" - - it "removes the error message when is a valid name", -> - fillNameWith 'foo?bar' - expect($('.js-branch-name-error span').length).toEqual(1) - fillNameWith 'foobar' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have dashes anywhere", -> - fillNameWith '-foo-bar-zoo-' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have underscores anywhere", -> - fillNameWith '_foo_bar_zoo_' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have numbers anywhere", -> - fillNameWith '1foo2bar3zoo4' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can be only letters", -> - fillNameWith 'foo' - expect($('.js-branch-name-error span').length).toEqual(0) diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js new file mode 100644 index 00000000000..14dc6bfdfde --- /dev/null +++ b/spec/javascripts/notes_spec.js @@ -0,0 +1,41 @@ + +/*= require notes */ + + +/*= require gl_form */ + +(function() { + window.gon || (window.gon = {}); + + window.disableButtonIfEmptyField = function() { + return null; + }; + + describe('Notes', function() { + return describe('task lists', function() { + fixture.preload('issue_note.html'); + beforeEach(function() { + fixture.load('issue_note.html'); + $('form').on('submit', function(e) { + return e.preventDefault(); + }); + return this.notes = new Notes(); + }); + it('modifies the Markdown field', function() { + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits the form on tasklist:changed', function() { + var submitted; + submitted = false; + $('form').on('submit', function(e) { + submitted = true; + return e.preventDefault(); + }); + $('.js-task-list-field').trigger('tasklist:changed'); + return expect(submitted).toBe(true); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee deleted file mode 100644 index 3a3c8d63e82..00000000000 --- a/spec/javascripts/notes_spec.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -#= require notes -#= require gl_form - -window.gon or= {} -window.disableButtonIfEmptyField = -> null - -describe 'Notes', -> - describe 'task lists', -> - fixture.preload('issue_note.html') - - beforeEach -> - fixture.load('issue_note.html') - $('form').on 'submit', (e) -> e.preventDefault() - - @notes = new Notes() - - it 'modifies the Markdown field', -> - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits the form on tasklist:changed', -> - submitted = false - $('form').on 'submit', (e) -> submitted = true; e.preventDefault() - - $('.js-task-list-field').trigger('tasklist:changed') - expect(submitted).toBe(true) diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js new file mode 100644 index 00000000000..ffe49828492 --- /dev/null +++ b/spec/javascripts/project_title_spec.js @@ -0,0 +1,60 @@ + +/*= require bootstrap */ + + +/*= require select2 */ + + +/*= require lib/utils/type_utility */ + + +/*= require gl_dropdown */ + + +/*= require api */ + + +/*= require project_select */ + + +/*= require project */ + +(function() { + window.gon || (window.gon = {}); + + window.gon.api_version = 'v3'; + + describe('Project Title', function() { + fixture.preload('project_title.html'); + fixture.preload('projects.json'); + beforeEach(function() { + fixture.load('project_title.html'); + return this.project = new Project(); + }); + return describe('project list', function() { + beforeEach((function(_this) { + return function() { + _this.projects_data = fixture.load('projects.json')[0]; + return spyOn(jQuery, 'ajax').and.callFake(function(req) { + var d; + expect(req.url).toBe('/api/v3/projects.json?simple=true'); + d = $.Deferred(); + d.resolve(_this.projects_data); + return d.promise(); + }); + }; + })(this)); + it('to show on toggle click', (function(_this) { + return function() { + $('.js-projects-dropdown-toggle').click(); + return expect($('.header-content').hasClass('open')).toBe(true); + }; + })(this)); + return it('hide dropdown', function() { + $(".dropdown-menu-close-icon").click(); + return expect($('.header-content').hasClass('open')).toBe(false); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee deleted file mode 100644 index 0244119fa0e..00000000000 --- a/spec/javascripts/project_title_spec.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -#= require bootstrap -#= require select2 -#= require lib/utils/type_utility -#= require gl_dropdown -#= require api -#= require project_select -#= require project - -window.gon or= {} -window.gon.api_version = 'v3' - -describe 'Project Title', -> - fixture.preload('project_title.html') - fixture.preload('projects.json') - - beforeEach -> - fixture.load('project_title.html') - @project = new Project() - - describe 'project list', -> - beforeEach => - @projects_data = fixture.load('projects.json')[0] - - spyOn(jQuery, 'ajax').and.callFake (req) => - expect(req.url).toBe('/api/v3/projects.json?simple=true') - d = $.Deferred() - d.resolve @projects_data - d.promise() - - it 'to show on toggle click', => - $('.js-projects-dropdown-toggle').click() - expect($('.header-content').hasClass('open')).toBe(true) - - it 'hide dropdown', -> - $(".dropdown-menu-close-icon").click() - - expect($('.header-content').hasClass('open')).toBe(false) diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js new file mode 100644 index 00000000000..38b3b2653ec --- /dev/null +++ b/spec/javascripts/right_sidebar_spec.js @@ -0,0 +1,70 @@ + +/*= require right_sidebar */ + + +/*= require jquery */ + + +/*= require jquery.cookie */ + +(function() { + var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; + + this.sidebar = null; + + $aside = null; + + $toggle = null; + + $icon = null; + + $page = null; + + $labelsIcon = null; + + assertSidebarState = function(state) { + var shouldBeCollapsed, shouldBeExpanded; + shouldBeExpanded = state === 'expanded'; + shouldBeCollapsed = state === 'collapsed'; + expect($aside.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); + expect($page.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); + expect($icon.hasClass('fa-angle-double-right')).toBe(shouldBeExpanded); + expect($aside.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed); + expect($page.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed); + return expect($icon.hasClass('fa-angle-double-left')).toBe(shouldBeCollapsed); + }; + + describe('RightSidebar', function() { + fixture.preload('right_sidebar.html'); + beforeEach(function() { + fixture.load('right_sidebar.html'); + this.sidebar = new Sidebar; + $aside = $('.right-sidebar'); + $page = $('.page-with-sidebar'); + $icon = $aside.find('i'); + $toggle = $aside.find('.js-sidebar-toggle'); + return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); + }); + it('should expand the sidebar when arrow is clicked', function() { + $toggle.click(); + return assertSidebarState('expanded'); + }); + it('should collapse the sidebar when arrow is clicked', function() { + $toggle.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + it('should float over the page and when sidebar icons clicked', function() { + $labelsIcon.click(); + return assertSidebarState('expanded'); + }); + return it('should collapse when the icon arrow clicked while it is floating on page', function() { + $labelsIcon.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + }); + +}).call(this); diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee deleted file mode 100644 index 2075cacdb67..00000000000 --- a/spec/javascripts/right_sidebar_spec.js.coffee +++ /dev/null @@ -1,69 +0,0 @@ -#= require right_sidebar -#= require jquery -#= require jquery.cookie - -@sidebar = null -$aside = null -$toggle = null -$icon = null -$page = null -$labelsIcon = null - - -assertSidebarState = (state) -> - - shouldBeExpanded = state is 'expanded' - shouldBeCollapsed = state is 'collapsed' - - expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded - expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded - expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded - - expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed - expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed - expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed - - -describe 'RightSidebar', -> - - fixture.preload 'right_sidebar.html' - - beforeEach -> - fixture.load 'right_sidebar.html' - - @sidebar = new Sidebar - $aside = $ '.right-sidebar' - $page = $ '.page-with-sidebar' - $icon = $aside.find 'i' - $toggle = $aside.find '.js-sidebar-toggle' - $labelsIcon = $aside.find '.sidebar-collapsed-icon' - - - it 'should expand the sidebar when arrow is clicked', -> - - $toggle.click() - assertSidebarState 'expanded' - - - it 'should collapse the sidebar when arrow is clicked', -> - - $toggle.click() - assertSidebarState 'expanded' - - $toggle.click() - assertSidebarState 'collapsed' - - - it 'should float over the page and when sidebar icons clicked', -> - - $labelsIcon.click() - assertSidebarState 'expanded' - - - it 'should collapse when the icon arrow clicked while it is floating on page', -> - - $labelsIcon.click() - assertSidebarState 'expanded' - - $toggle.click() - assertSidebarState 'collapsed' diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js new file mode 100644 index 00000000000..68d64483d67 --- /dev/null +++ b/spec/javascripts/search_autocomplete_spec.js @@ -0,0 +1,159 @@ + +/*= require gl_dropdown */ + + +/*= require search_autocomplete */ + + +/*= require jquery */ + + +/*= require lib/utils/common_utils */ + + +/*= require lib/utils/type_utility */ + + +/*= require fuzzaldrin-plus */ + +(function() { + var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + + widget = null; + + userId = 1; + + window.gon || (window.gon = {}); + + window.gon.current_user_id = userId; + + dashboardIssuesPath = '/dashboard/issues'; + + dashboardMRsPath = '/dashboard/merge_requests'; + + projectIssuesPath = '/gitlab-org/gitlab-ce/issues'; + + projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'; + + groupIssuesPath = '/groups/gitlab-org/issues'; + + groupMRsPath = '/groups/gitlab-org/merge_requests'; + + projectName = 'GitLab Community Edition'; + + groupName = 'Gitlab Org'; + + addBodyAttributes = function(section) { + var $body; + if (section == null) { + section = 'dashboard'; + } + $body = $('body'); + $body.removeAttr('data-page'); + $body.removeAttr('data-project'); + $body.removeAttr('data-group'); + switch (section) { + case 'dashboard': + return $body.data('page', 'root:index'); + case 'group': + $body.data('page', 'groups:show'); + return $body.data('group', 'gitlab-org'); + case 'project': + $body.data('page', 'projects:show'); + return $body.data('project', 'gitlab-ce'); + } + }; + + mockDashboardOptions = function() { + window.gl || (window.gl = {}); + return window.gl.dashboardOptions = { + issuesPath: dashboardIssuesPath, + mrPath: dashboardMRsPath + }; + }; + + mockProjectOptions = function() { + window.gl || (window.gl = {}); + return window.gl.projectOptions = { + 'gitlab-ce': { + issuesPath: projectIssuesPath, + mrPath: projectMRsPath, + projectName: projectName + } + }; + }; + + mockGroupOptions = function() { + window.gl || (window.gl = {}); + return window.gl.groupOptions = { + 'gitlab-org': { + issuesPath: groupIssuesPath, + mrPath: groupMRsPath, + projectName: groupName + } + }; + }; + + assertLinks = function(list, issuesPath, mrsPath) { + var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; + issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; + issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; + mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; + a1 = "a[href='" + issuesAssignedToMeLink + "']"; + a2 = "a[href='" + issuesIHaveCreatedLink + "']"; + a3 = "a[href='" + mrsAssignedToMeLink + "']"; + a4 = "a[href='" + mrsIHaveCreatedLink + "']"; + expect(list.find(a1).length).toBe(1); + expect(list.find(a1).text()).toBe(' Issues assigned to me '); + expect(list.find(a2).length).toBe(1); + expect(list.find(a2).text()).toBe(" Issues I've created "); + expect(list.find(a3).length).toBe(1); + expect(list.find(a3).text()).toBe(' Merge requests assigned to me '); + expect(list.find(a4).length).toBe(1); + return expect(list.find(a4).text()).toBe(" Merge requests I've created "); + }; + + describe('Search autocomplete dropdown', function() { + fixture.preload('search_autocomplete.html'); + beforeEach(function() { + fixture.load('search_autocomplete.html'); + return widget = new SearchAutocomplete; + }); + it('should show Dashboard specific dropdown menu', function() { + var list; + addBodyAttributes(); + mockDashboardOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); + }); + it('should show Group specific dropdown menu', function() { + var list; + addBodyAttributes('group'); + mockGroupOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, groupIssuesPath, groupMRsPath); + }); + it('should show Project specific dropdown menu', function() { + var list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, projectIssuesPath, projectMRsPath); + }); + return it('should not show category related menu if there is text in the input', function() { + var link, list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.val('help'); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']"; + return expect(list.find(link).length).toBe(0); + }); + }); + +}).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee deleted file mode 100644 index 1c1faca3333..00000000000 --- a/spec/javascripts/search_autocomplete_spec.js.coffee +++ /dev/null @@ -1,149 +0,0 @@ -#= require gl_dropdown -#= require search_autocomplete -#= require jquery -#= require lib/utils/common_utils -#= require lib/utils/type_utility -#= require fuzzaldrin-plus - - -widget = null -userId = 1 -window.gon or= {} -window.gon.current_user_id = userId - -dashboardIssuesPath = '/dashboard/issues' -dashboardMRsPath = '/dashboard/merge_requests' -projectIssuesPath = '/gitlab-org/gitlab-ce/issues' -projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests' -groupIssuesPath = '/groups/gitlab-org/issues' -groupMRsPath = '/groups/gitlab-org/merge_requests' -projectName = 'GitLab Community Edition' -groupName = 'Gitlab Org' - - -# Add required attributes to body before starting the test. -# section would be dashboard|group|project -addBodyAttributes = (section = 'dashboard') -> - - $body = $ 'body' - - $body.removeAttr 'data-page' - $body.removeAttr 'data-project' - $body.removeAttr 'data-group' - - switch section - when 'dashboard' - $body.data 'page', 'root:index' - when 'group' - $body.data 'page', 'groups:show' - $body.data 'group', 'gitlab-org' - when 'project' - $body.data 'page', 'projects:show' - $body.data 'project', 'gitlab-ce' - - -# Mock `gl` object in window for dashboard specific page. App code will need it. -mockDashboardOptions = -> - - window.gl or= {} - window.gl.dashboardOptions = - issuesPath: dashboardIssuesPath - mrPath : dashboardMRsPath - - -# Mock `gl` object in window for project specific page. App code will need it. -mockProjectOptions = -> - - window.gl or= {} - window.gl.projectOptions = - 'gitlab-ce' : - issuesPath : projectIssuesPath - mrPath : projectMRsPath - projectName : projectName - - -mockGroupOptions = -> - - window.gl or= {} - window.gl.groupOptions = - 'gitlab-org' : - issuesPath : groupIssuesPath - mrPath : groupMRsPath - projectName : groupName - - -assertLinks = (list, issuesPath, mrsPath) -> - - issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}" - issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}" - mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}" - mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}" - - a1 = "a[href='#{issuesAssignedToMeLink}']" - a2 = "a[href='#{issuesIHaveCreatedLink}']" - a3 = "a[href='#{mrsAssignedToMeLink}']" - a4 = "a[href='#{mrsIHaveCreatedLink}']" - - expect(list.find(a1).length).toBe 1 - expect(list.find(a1).text()).toBe ' Issues assigned to me ' - - expect(list.find(a2).length).toBe 1 - expect(list.find(a2).text()).toBe " Issues I've created " - - expect(list.find(a3).length).toBe 1 - expect(list.find(a3).text()).toBe ' Merge requests assigned to me ' - - expect(list.find(a4).length).toBe 1 - expect(list.find(a4).text()).toBe " Merge requests I've created " - - -describe 'Search autocomplete dropdown', -> - - fixture.preload 'search_autocomplete.html' - - beforeEach -> - - fixture.load 'search_autocomplete.html' - widget = new SearchAutocomplete - - - it 'should show Dashboard specific dropdown menu', -> - - addBodyAttributes() - mockDashboardOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, dashboardIssuesPath, dashboardMRsPath - - - it 'should show Group specific dropdown menu', -> - - addBodyAttributes 'group' - mockGroupOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, groupIssuesPath, groupMRsPath - - - it 'should show Project specific dropdown menu', -> - - addBodyAttributes 'project' - mockProjectOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, projectIssuesPath, projectMRsPath - - - it 'should not show category related menu if there is text in the input', -> - - addBodyAttributes 'project' - mockProjectOptions() - widget.searchInput.val 'help' - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']" - expect(list.find(link).length).toBe 0 diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js new file mode 100644 index 00000000000..7b6b55fe545 --- /dev/null +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -0,0 +1,74 @@ + +/*= require shortcuts_issuable */ + +(function() { + describe('ShortcutsIssuable', function() { + fixture.preload('issuable.html'); + beforeEach(function() { + fixture.load('issuable.html'); + return this.shortcut = new ShortcutsIssuable(); + }); + return describe('#replyWithSelectedText', function() { + var stubSelection; + stubSelection = function(text) { + return window.getSelection = function() { + return text; + }; + }; + beforeEach(function() { + return this.selector = 'form.js-main-target-form textarea#note_note'; + }); + describe('with empty selection', function() { + return it('does nothing', function() { + stubSelection(''); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe(''); + }); + }); + describe('with any selection', function() { + beforeEach(function() { + return stubSelection('Selected text.'); + }); + it('leaves existing input intact', function() { + $(this.selector).val('This text was already here.'); + expect($(this.selector).val()).toBe('This text was already here.'); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n"); + }); + it('triggers `input`', function() { + var triggered; + triggered = false; + $(this.selector).on('input', function() { + return triggered = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(triggered).toBe(true); + }); + return it('triggers `focus`', function() { + var focused; + focused = false; + $(this.selector).on('focus', function() { + return focused = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(focused).toBe(true); + }); + }); + describe('with a one-line selection', function() { + return it('quotes the selection', function() { + stubSelection('This text has been selected.'); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); + }); + }); + return describe('with a multi-line selection', function() { + return it('quotes the selected lines as a group', function() { + stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n"); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n"); + }); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee deleted file mode 100644 index a01ad7140dd..00000000000 --- a/spec/javascripts/shortcuts_issuable_spec.js.coffee +++ /dev/null @@ -1,82 +0,0 @@ -#= require shortcuts_issuable - -describe 'ShortcutsIssuable', -> - fixture.preload('issuable.html') - - beforeEach -> - fixture.load('issuable.html') - @shortcut = new ShortcutsIssuable() - - describe '#replyWithSelectedText', -> - # Stub window.getSelection to return the provided String. - stubSelection = (text) -> - window.getSelection = -> text - - beforeEach -> - @selector = 'form.js-main-target-form textarea#note_note' - - describe 'with empty selection', -> - it 'does nothing', -> - stubSelection('') - @shortcut.replyWithSelectedText() - expect($(@selector).val()).toBe('') - - describe 'with any selection', -> - beforeEach -> - stubSelection('Selected text.') - - it 'leaves existing input intact', -> - $(@selector).val('This text was already here.') - expect($(@selector).val()).toBe('This text was already here.') - - @shortcut.replyWithSelectedText() - expect($(@selector).val()). - toBe("This text was already here.\n> Selected text.\n\n") - - it 'triggers `input`', -> - triggered = false - $(@selector).on 'input', -> triggered = true - @shortcut.replyWithSelectedText() - - expect(triggered).toBe(true) - - it 'triggers `focus`', -> - focused = false - $(@selector).on 'focus', -> focused = true - @shortcut.replyWithSelectedText() - - expect(focused).toBe(true) - - describe 'with a one-line selection', -> - it 'quotes the selection', -> - stubSelection('This text has been selected.') - - @shortcut.replyWithSelectedText() - - expect($(@selector).val()). - toBe("> This text has been selected.\n\n") - - describe 'with a multi-line selection', -> - it 'quotes the selected lines as a group', -> - stubSelection( - """ - Selected line one. - - Selected line two. - Selected line three. - - """ - ) - - @shortcut.replyWithSelectedText() - - expect($(@selector).val()). - toBe( - """ - > Selected line one. - > Selected line two. - > Selected line three. - - - """ - ) diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee deleted file mode 100644 index 90b02a6aec5..00000000000 --- a/spec/javascripts/spec_helper.coffee +++ /dev/null @@ -1,47 +0,0 @@ -# PhantomJS (Teaspoons default driver) doesn't have support for -# Function.prototype.bind, which has caused confusion. Use this polyfill to -# avoid the confusion. - -#= require support/bind-poly - -# You can require your own javascript files here. By default this will include -# everything in application, however you may get better load performance if you -# require the specific files that are being used in the spec that tests them. - -#= require jquery -#= require jquery.turbolinks -#= require bootstrap -#= require underscore - -# Teaspoon includes some support files, but you can use anything from your own -# support path too. - -# require support/jasmine-jquery-1.7.0 -# require support/jasmine-jquery-2.0.0 -#= require support/jasmine-jquery-2.1.0 -# require support/sinon -# require support/your-support-file - -# Deferring execution - -# If you're using CommonJS, RequireJS or some other asynchronous library you can -# defer execution. Call Teaspoon.execute() after everything has been loaded. -# Simple example of a timeout: - -# Teaspoon.defer = true -# setTimeout(Teaspoon.execute, 1000) - -# Matching files - -# By default Teaspoon will look for files that match -# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path -# and it'll be included in the default suite automatically. If you want to -# customize suites, check out the configuration in teaspoon_env.rb - -# Manifest - -# If you'd rather require your spec files manually (to control order for -# instance) you can disable the suite matcher in the configuration and use this -# file as a manifest. - -# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js new file mode 100644 index 00000000000..7d91ed0f855 --- /dev/null +++ b/spec/javascripts/spec_helper.js @@ -0,0 +1,22 @@ + +/*= require support/bind-poly */ + + +/*= require jquery */ + + +/*= require jquery.turbolinks */ + + +/*= require bootstrap */ + + +/*= require underscore */ + + +/*= require support/jasmine-jquery-2.1.0 */ + +(function() { + + +}).call(this); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js new file mode 100644 index 00000000000..4e5dd1e59bf --- /dev/null +++ b/spec/javascripts/syntax_highlight_spec.js @@ -0,0 +1,44 @@ + +/*= require syntax_highlight */ + +(function() { + describe('Syntax Highlighter', function() { + var stubUserColorScheme; + stubUserColorScheme = function(value) { + if (window.gon == null) { + window.gon = {}; + } + return window.gon.user_color_scheme = value; + }; + describe('on a js-syntax-highlight element', function() { + beforeEach(function() { + return fixture.set('<div class="js-syntax-highlight"></div>'); + }); + return it('applies syntax highlighting', function() { + stubUserColorScheme('monokai'); + $('.js-syntax-highlight').syntaxHighlight(); + return expect($('.js-syntax-highlight')).toHaveClass('monokai'); + }); + }); + return describe('on a parent element', function() { + beforeEach(function() { + return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + }); + it('applies highlighting to all applicable children', function() { + stubUserColorScheme('monokai'); + $('.parent').syntaxHighlight(); + expect($('.parent, .foo')).not.toHaveClass('monokai'); + return expect($('.monokai').length).toBe(2); + }); + return it('prevents an infinite loop when no matches exist', function() { + var highlight; + fixture.set('<div></div>'); + highlight = function() { + return $('div').syntaxHighlight(); + }; + return expect(highlight).not.toThrow(); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee deleted file mode 100644 index 6a73b6bf32c..00000000000 --- a/spec/javascripts/syntax_highlight_spec.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -#= require syntax_highlight - -describe 'Syntax Highlighter', -> - stubUserColorScheme = (value) -> - window.gon ?= {} - window.gon.user_color_scheme = value - - describe 'on a js-syntax-highlight element', -> - beforeEach -> - fixture.set('<div class="js-syntax-highlight"></div>') - - it 'applies syntax highlighting', -> - stubUserColorScheme('monokai') - - $('.js-syntax-highlight').syntaxHighlight() - - expect($('.js-syntax-highlight')).toHaveClass('monokai') - - describe 'on a parent element', -> - beforeEach -> - fixture.set """ - <div class="parent"> - <div class="js-syntax-highlight"></div> - <div class="foo"></div> - <div class="js-syntax-highlight"></div> - </div> - """ - - it 'applies highlighting to all applicable children', -> - stubUserColorScheme('monokai') - - $('.parent').syntaxHighlight() - - expect($('.parent, .foo')).not.toHaveClass('monokai') - expect($('.monokai').length).toBe(2) - - it 'prevents an infinite loop when no matches exist', -> - fixture.set('<div></div>') - - highlight = -> $('div').syntaxHighlight() - - expect(highlight).not.toThrow() diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee deleted file mode 100644 index 8ffeda11704..00000000000 --- a/spec/javascripts/u2f/authenticate_spec.coffee +++ /dev/null @@ -1,51 +0,0 @@ -#= require u2f/authenticate -#= require u2f/util -#= require u2f/error -#= require u2f -#= require ./mock_u2f_device - -describe 'U2FAuthenticate', -> - fixture.load('u2f/authenticate') - - beforeEach -> - @u2fDevice = new MockU2FDevice - @container = $("#js-authenticate-u2f") - @component = new U2FAuthenticate(@container, {sign_requests: []}, "token") - @component.start() - - it 'allows authenticating via a U2F device', -> - setupButton = @container.find("#js-login-u2f-device") - setupMessage = @container.find("p") - expect(setupMessage.text()).toContain('Insert your security key') - expect(setupButton.text()).toBe('Login Via U2F Device') - setupButton.trigger('click') - - inProgressMessage = @container.find("p") - expect(inProgressMessage.text()).toContain("Trying to communicate with your device") - - @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) - authenticatedMessage = @container.find("p") - deviceResponse = @container.find('#js-device-response') - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') - - describe "errors", -> - it "displays an error message", -> - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("There was a problem communicating with your device") - - it "allows retrying authentication after an error", -> - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) - retryButton = @container.find("#js-u2f-try-again") - retryButton.trigger('click') - - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) - authenticatedMessage = @container.find("p") - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js new file mode 100644 index 00000000000..e008ce956ad --- /dev/null +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -0,0 +1,75 @@ + +/*= require u2f/authenticate */ + + +/*= require u2f/util */ + + +/*= require u2f/error */ + + +/*= require u2f */ + + +/*= require ./mock_u2f_device */ + +(function() { + describe('U2FAuthenticate', function() { + fixture.load('u2f/authenticate'); + beforeEach(function() { + this.u2fDevice = new MockU2FDevice; + this.container = $("#js-authenticate-u2f"); + this.component = new U2FAuthenticate(this.container, { + sign_requests: [] + }, "token"); + return this.component.start(); + }); + it('allows authenticating via a U2F device', function() { + var authenticatedMessage, deviceResponse, inProgressMessage, setupButton, setupMessage; + setupButton = this.container.find("#js-login-u2f-device"); + setupMessage = this.container.find("p"); + expect(setupMessage.text()).toContain('Insert your security key'); + expect(setupButton.text()).toBe('Login Via U2F Device'); + setupButton.trigger('click'); + inProgressMessage = this.container.find("p"); + expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: "this is data from the device" + }); + authenticatedMessage = this.container.find("p"); + deviceResponse = this.container.find('#js-device-response'); + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + return describe("errors", function() { + it("displays an error message", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: "error!" + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + }); + return it("allows retrying authentication after an error", function() { + var authenticatedMessage, retryButton, setupButton; + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: "error!" + }); + retryButton = this.container.find("#js-u2f-try-again"); + retryButton.trigger('click'); + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: "this is data from the device" + }); + authenticatedMessage = this.container.find("p"); + return expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js new file mode 100644 index 00000000000..ca91a716ba3 --- /dev/null +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -0,0 +1,33 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MockU2FDevice = (function() { + function MockU2FDevice() { + this.respondToAuthenticateRequest = bind(this.respondToAuthenticateRequest, this); + this.respondToRegisterRequest = bind(this.respondToRegisterRequest, this); + window.u2f || (window.u2f = {}); + window.u2f.register = (function(_this) { + return function(appId, registerRequests, signRequests, callback) { + return _this.registerCallback = callback; + }; + })(this); + window.u2f.sign = (function(_this) { + return function(appId, challenges, signRequests, callback) { + return _this.authenticateCallback = callback; + }; + })(this); + } + + MockU2FDevice.prototype.respondToRegisterRequest = function(params) { + return this.registerCallback(params); + }; + + MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) { + return this.authenticateCallback(params); + }; + + return MockU2FDevice; + + })(); + +}).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee deleted file mode 100644 index 97ed0e83a0e..00000000000 --- a/spec/javascripts/u2f/mock_u2f_device.js.coffee +++ /dev/null @@ -1,15 +0,0 @@ -class @MockU2FDevice - constructor: () -> - window.u2f ||= {} - - window.u2f.register = (appId, registerRequests, signRequests, callback) => - @registerCallback = callback - - window.u2f.sign = (appId, challenges, signRequests, callback) => - @authenticateCallback = callback - - respondToRegisterRequest: (params) => - @registerCallback(params) - - respondToAuthenticateRequest: (params) => - @authenticateCallback(params) diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js new file mode 100644 index 00000000000..21c5266c60e --- /dev/null +++ b/spec/javascripts/u2f/register_spec.js @@ -0,0 +1,81 @@ + +/*= require u2f/register */ + + +/*= require u2f/util */ + + +/*= require u2f/error */ + + +/*= require u2f */ + + +/*= require ./mock_u2f_device */ + +(function() { + describe('U2FRegister', function() { + fixture.load('u2f/register'); + beforeEach(function() { + this.u2fDevice = new MockU2FDevice; + this.container = $("#js-register-u2f"); + this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); + return this.component.start(); + }); + it('allows registering a U2F device', function() { + var deviceResponse, inProgressMessage, registeredMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + expect(setupButton.text()).toBe('Setup New U2F Device'); + setupButton.trigger('click'); + inProgressMessage = this.container.children("p"); + expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + this.u2fDevice.respondToRegisterRequest({ + deviceData: "this is data from the device" + }); + registeredMessage = this.container.find('p'); + deviceResponse = this.container.find('#js-device-response'); + expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + return describe("errors", function() { + it("doesn't allow the same device to be registered twice (for the same user", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: 4 + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("already been registered with us"); + }); + it("displays an error message for other errors", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: "error!" + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + }); + return it("allows retrying registration after an error", function() { + var registeredMessage, retryButton, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: "error!" + }); + retryButton = this.container.find("#U2FTryAgain"); + retryButton.trigger('click'); + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + deviceData: "this is data from the device" + }); + registeredMessage = this.container.find("p"); + return expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee deleted file mode 100644 index 87dc769792b..00000000000 --- a/spec/javascripts/u2f/register_spec.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -#= require u2f/register -#= require u2f/util -#= require u2f/error -#= require u2f -#= require ./mock_u2f_device - -describe 'U2FRegister', -> - fixture.load('u2f/register') - - beforeEach -> - @u2fDevice = new MockU2FDevice - @container = $("#js-register-u2f") - @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token") - @component.start() - - it 'allows registering a U2F device', -> - setupButton = @container.find("#js-setup-u2f-device") - expect(setupButton.text()).toBe('Setup New U2F Device') - setupButton.trigger('click') - - inProgressMessage = @container.children("p") - expect(inProgressMessage.text()).toContain("Trying to communicate with your device") - - @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) - registeredMessage = @container.find('p') - deviceResponse = @container.find('#js-device-response') - expect(registeredMessage.text()).toContain("Your device was successfully set up!") - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') - - describe "errors", -> - it "doesn't allow the same device to be registered twice (for the same user", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: 4}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("already been registered with us") - - it "displays an error message for other errors", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("There was a problem communicating with your device") - - it "allows retrying registration after an error", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) - retryButton = @container.find("#U2FTryAgain") - retryButton.trigger('click') - - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) - registeredMessage = @container.find("p") - expect(registeredMessage.text()).toContain("Your device was successfully set up!") diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js new file mode 100644 index 00000000000..3d680ec8ea3 --- /dev/null +++ b/spec/javascripts/zen_mode_spec.js @@ -0,0 +1,73 @@ + +/*= require zen_mode */ + +(function() { + var enterZen, escapeKeydown, exitZen; + + describe('ZenMode', function() { + fixture.preload('zen_mode.html'); + beforeEach(function() { + fixture.load('zen_mode.html'); + spyOn(Dropzone, 'forElement').and.callFake(function() { + return { + enable: function() { + return true; + } + }; + }); + this.zen = new ZenMode(); + return this.zen.scroll_position = 456; + }); + describe('on enter', function() { + it('pauses Mousetrap', function() { + spyOn(Mousetrap, 'pause'); + enterZen(); + return expect(Mousetrap.pause).toHaveBeenCalled(); + }); + return it('removes textarea styling', function() { + $('textarea').attr('style', 'height: 400px'); + enterZen(); + return expect('textarea').not.toHaveAttr('style'); + }); + }); + describe('in use', function() { + beforeEach(function() { + return enterZen(); + }); + return it('exits on Escape', function() { + escapeKeydown(); + return expect($('.zen-backdrop')).not.toHaveClass('fullscreen'); + }); + }); + return describe('on exit', function() { + beforeEach(function() { + return enterZen(); + }); + it('unpauses Mousetrap', function() { + spyOn(Mousetrap, 'unpause'); + exitZen(); + return expect(Mousetrap.unpause).toHaveBeenCalled(); + }); + return it('restores the scroll position', function() { + spyOn(this.zen, 'scrollTo'); + exitZen(); + return expect(this.zen.scrollTo).toHaveBeenCalled(); + }); + }); + }); + + enterZen = function() { + return $('a.js-zen-enter').click(); + }; + + exitZen = function() { + return $('a.js-zen-leave').click(); + }; + + escapeKeydown = function() { + return $('textarea').trigger($.Event('keydown', { + keyCode: 27 + })); + }; + +}).call(this); diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee deleted file mode 100644 index b790fce01ed..00000000000 --- a/spec/javascripts/zen_mode_spec.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -#= require zen_mode - -describe 'ZenMode', -> - fixture.preload('zen_mode.html') - - beforeEach -> - fixture.load('zen_mode.html') - - # Stub Dropzone.forElement(...).enable() - spyOn(Dropzone, 'forElement').and.callFake -> - enable: -> true - - @zen = new ZenMode() - - # Set this manually because we can't actually scroll the window - @zen.scroll_position = 456 - - describe 'on enter', -> - it 'pauses Mousetrap', -> - spyOn(Mousetrap, 'pause') - enterZen() - expect(Mousetrap.pause).toHaveBeenCalled() - - it 'removes textarea styling', -> - $('textarea').attr('style', 'height: 400px') - enterZen() - expect('textarea').not.toHaveAttr('style') - - describe 'in use', -> - beforeEach -> enterZen() - - it 'exits on Escape', -> - escapeKeydown() - expect($('.zen-backdrop')).not.toHaveClass('fullscreen') - - describe 'on exit', -> - beforeEach -> enterZen() - - it 'unpauses Mousetrap', -> - spyOn(Mousetrap, 'unpause') - exitZen() - expect(Mousetrap.unpause).toHaveBeenCalled() - - it 'restores the scroll position', -> - spyOn(@zen, 'scrollTo') - exitZen() - expect(@zen.scrollTo).toHaveBeenCalled() - -enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm -exitZen = -> $('a.js-zen-leave').click() -escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27})) diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js new file mode 100644 index 00000000000..bc451506b6a --- /dev/null +++ b/vendor/assets/javascripts/task_list.js @@ -0,0 +1,119 @@ + +/*= provides tasklist:enabled */ + + +/*= provides tasklist:disabled */ + + +/*= provides tasklist:change */ + + +/*= provides tasklist:changed */ + +(function() { + var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem, + 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; }; + + incomplete = "[ ]"; + + complete = "[x]"; + + escapePattern = function(str) { + return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]"); + }; + + incompletePattern = RegExp("" + (escapePattern(incomplete))); + + completePattern = RegExp("" + (escapePattern(complete))); + + itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))"); + + codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg; + + itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g"); + + updateTaskListItem = function(source, itemIndex, checked) { + var clean, index, line, result; + clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n"); + index = 0; + result = (function() { + var i, len, ref, results; + ref = source.split("\n"); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + line = ref[i]; + if (indexOf.call(clean, line) >= 0 && line.match(itemPattern)) { + index += 1; + if (index === itemIndex) { + line = checked ? line.replace(incompletePattern, complete) : line.replace(completePattern, incomplete); + } + } + results.push(line); + } + return results; + })(); + return result.join("\n"); + }; + + updateTaskList = function($item) { + var $container, $field, checked, event, index; + $container = $item.closest('.js-task-list-container'); + $field = $container.find('.js-task-list-field'); + index = 1 + $container.find('.task-list-item-checkbox').index($item); + checked = $item.prop('checked'); + event = $.Event('tasklist:change'); + $field.trigger(event, [index, checked]); + if (!event.isDefaultPrevented()) { + $field.val(updateTaskListItem($field.val(), index, checked)); + $field.trigger('change'); + return $field.trigger('tasklist:changed', [index, checked]); + } + }; + + $(document).on('change', '.task-list-item-checkbox', function() { + return updateTaskList($(this)); + }); + + enableTaskList = function($container) { + if ($container.find('.js-task-list-field').length > 0) { + $container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null); + return $container.addClass('is-task-list-enabled').trigger('tasklist:enabled'); + } + }; + + enableTaskLists = function($containers) { + var container, i, len, results; + results = []; + for (i = 0, len = $containers.length; i < len; i++) { + container = $containers[i]; + results.push(enableTaskList($(container))); + } + return results; + }; + + disableTaskList = function($container) { + $container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled'); + return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled'); + }; + + disableTaskLists = function($containers) { + var container, i, len, results; + results = []; + for (i = 0, len = $containers.length; i < len; i++) { + container = $containers[i]; + results.push(disableTaskList($(container))); + } + return results; + }; + + $.fn.taskList = function(method) { + var $container, methods; + $container = $(this).closest('.js-task-list-container'); + methods = { + enable: enableTaskLists, + disable: disableTaskLists + }; + return methods[method || 'enable']($container); + }; + +}).call(this); diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee deleted file mode 100644 index 584751af8ea..00000000000 --- a/vendor/assets/javascripts/task_list.js.coffee +++ /dev/null @@ -1,258 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2014 GitHub, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# TaskList Behavior -# -#= provides tasklist:enabled -#= provides tasklist:disabled -#= provides tasklist:change -#= provides tasklist:changed -# -# -# Enables Task List update behavior. -# -# ### Example Markup -# -# <div class="js-task-list-container"> -# <ul class="task-list"> -# <li class="task-list-item"> -# <input type="checkbox" class="js-task-list-item-checkbox" disabled /> -# text -# </li> -# </ul> -# <form> -# <textarea class="js-task-list-field">- [ ] text</textarea> -# </form> -# </div> -# -# ### Specification -# -# TaskLists MUST be contained in a `(div).js-task-list-container`. -# -# TaskList Items SHOULD be an a list (`UL`/`OL`) element. -# -# Task list items MUST match `(input).task-list-item-checkbox` and MUST be -# `disabled` by default. -# -# TaskLists MUST have a `(textarea).js-task-list-field` form element whose -# `value` attribute is the source (Markdown) to be udpated. The source MUST -# follow the syntax guidelines. -# -# TaskList updates trigger `tasklist:change` events. If the change is -# successful, `tasklist:changed` is fired. The change can be canceled. -# -# jQuery is required. -# -# ### Methods -# -# `.taskList('enable')` or `.taskList()` -# -# Enables TaskList updates for the container. -# -# `.taskList('disable')` -# -# Disables TaskList updates for the container. -# -## ### Events -# -# `tasklist:enabled` -# -# Fired when the TaskList is enabled. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** No -# * **Target** `.js-task-list-container` -# -# `tasklist:disabled` -# -# Fired when the TaskList is disabled. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** No -# * **Target** `.js-task-list-container` -# -# `tasklist:change` -# -# Fired before the TaskList item change takes affect. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** Yes -# * **Target** `.js-task-list-field` -# -# `tasklist:changed` -# -# Fired once the TaskList item change has taken affect. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** No -# * **Target** `.js-task-list-field` -# -# ### NOTE -# -# Task list checkboxes are rendered as disabled by default because rendered -# user content is cached without regard for the viewer. - -incomplete = "[ ]" -complete = "[x]" - -# Escapes the String for regular expression matching. -escapePattern = (str) -> - str. - replace(/([\[\]])/g, "\\$1"). # escape square brackets - replace(/\s/, "\\s"). # match all white space - replace("x", "[xX]") # match all cases - -incompletePattern = /// - #{escapePattern(incomplete)} -/// -completePattern = /// - #{escapePattern(complete)} -/// - -# Pattern used to identify all task list items. -# Useful when you need iterate over all items. -itemPattern = /// - ^ - (?: # prefix, consisting of - \s* # optional leading whitespace - (?:>\s*)* # zero or more blockquotes - (?:[-+*]|(?:\d+\.)) # list item indicator - ) - \s* # optional whitespace prefix - ( # checkbox - #{escapePattern(complete)}| - #{escapePattern(incomplete)} - ) - \s+ # is followed by whitespace - (?! - \(.*?\) # is not part of a [foo](url) link - ) - (?= # and is followed by zero or more links - (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)* - (?:[^\[]|$) # and either a non-link or the end of the string - ) -/// - -# Used to filter out code fences from the source for comparison only. -# http://rubular.com/r/x5EwZVrloI -# Modified slightly due to issues with JS -codeFencesPattern = /// - ^`{3} # ``` - (?:\s*\w+)? # followed by optional language - [\S\s] # whitespace - .* # code - [\S\s] # whitespace - ^`{3}$ # ``` -///mg - -# Used to filter out potential mismatches (items not in lists). -# http://rubular.com/r/OInl6CiePy -itemsInParasPattern = /// - ^ - ( - #{escapePattern(complete)}| - #{escapePattern(incomplete)} - ) - .+ - $ -///g - -# Given the source text, updates the appropriate task list item to match the -# given checked value. -# -# Returns the updated String text. -updateTaskListItem = (source, itemIndex, checked) -> - clean = source.replace(/\r/g, '').replace(codeFencesPattern, ''). - replace(itemsInParasPattern, '').split("\n") - index = 0 - result = for line in source.split("\n") - if line in clean && line.match(itemPattern) - index += 1 - if index == itemIndex - line = - if checked - line.replace(incompletePattern, complete) - else - line.replace(completePattern, incomplete) - line - result.join("\n") - -# Updates the $field value to reflect the state of $item. -# Triggers the `tasklist:change` event before the value has changed, and fires -# a `tasklist:changed` event once the value has changed. -updateTaskList = ($item) -> - $container = $item.closest '.js-task-list-container' - $field = $container.find '.js-task-list-field' - index = 1 + $container.find('.task-list-item-checkbox').index($item) - checked = $item.prop 'checked' - - event = $.Event 'tasklist:change' - $field.trigger event, [index, checked] - - unless event.isDefaultPrevented() - $field.val updateTaskListItem($field.val(), index, checked) - $field.trigger 'change' - $field.trigger 'tasklist:changed', [index, checked] - -# When the task list item checkbox is updated, submit the change -$(document).on 'change', '.task-list-item-checkbox', -> - updateTaskList $(this) - -# Enables TaskList item changes. -enableTaskList = ($container) -> - if $container.find('.js-task-list-field').length > 0 - $container. - find('.task-list-item').addClass('enabled'). - find('.task-list-item-checkbox').attr('disabled', null) - $container.addClass('is-task-list-enabled'). - trigger 'tasklist:enabled' - -# Enables a collection of TaskList containers. -enableTaskLists = ($containers) -> - for container in $containers - enableTaskList $(container) - -# Disable TaskList item changes. -disableTaskList = ($container) -> - $container. - find('.task-list-item').removeClass('enabled'). - find('.task-list-item-checkbox').attr('disabled', 'disabled') - $container.removeClass('is-task-list-enabled'). - trigger 'tasklist:disabled' - -# Disables a collection of TaskList containers. -disableTaskLists = ($containers) -> - for container in $containers - disableTaskList $(container) - -$.fn.taskList = (method) -> - $container = $(this).closest('.js-task-list-container') - - methods = - enable: enableTaskLists - disable: disableTaskLists - - methods[method || 'enable']($container) |