diff options
author | Zeger-Jan van de Weg <zegerjan@gitlab.com> | 2016-03-29 17:26:59 +0200 |
---|---|---|
committer | Zeger-Jan van de Weg <zegerjan@gitlab.com> | 2016-03-29 17:26:59 +0200 |
commit | 3339513ca67c50a231b8906a33eccc0d209270a5 (patch) | |
tree | e9dbe77b814abd59062ba5dcdd3f454011d758cc /app/assets/javascripts | |
parent | 15a6633999c81387245cabf129dd2fbb04650c95 (diff) | |
parent | 54957d6932c2b159e01b60ee1d4e191cfdf5b713 (diff) | |
download | gitlab-ce-3339513ca67c50a231b8906a33eccc0d209270a5.tar.gz |
Merge branch 'master' into assign-to-issuable-opener
Diffstat (limited to 'app/assets/javascripts')
40 files changed, 1956 insertions, 263 deletions
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 3b6b453ac51..5092e824e65 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,7 +1,7 @@ class @Activities constructor: -> Pager.init 20, true - $(".event-filter a").bind "click", (event) => + $(".event-filter-link").on "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() @@ -12,18 +12,10 @@ class @Activities toggleFilter: (sender) -> - sender.closest('li').toggleClass "active" + $('.event-filter .active').removeClass "active" event_filters = $.cookie("event_filter") filter = sender.attr("id").split("_")[0] - if event_filters - event_filters = event_filters.split(",") - else - event_filters = new Array() + $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' } - index = event_filters.indexOf(filter) - if index is -1 - event_filters.push filter - else - event_filters.splice index, 1 - - $.cookie "event_filter", event_filters.join(","), { path: '/' } + if event_filters isnt filter + sender.closest('li').toggleClass "active" diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 3e0fdb3f795..f3ed9a66715 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -4,6 +4,7 @@ namespaces_path: "/api/:version/namespaces.json" group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" + labels_path: "/api/:version/projects/:id/labels" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -61,6 +62,21 @@ ).done (projects) -> callback(projects) + newLabel: (project_id, data, callback) -> + url = Api.buildUrl(Api.labels_path) + 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.group_projects_path) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 367bd098bfd..f01c67e9474 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -7,6 +7,7 @@ #= require jquery #= require jquery-ui/autocomplete #= require jquery-ui/datepicker +#= require jquery-ui/draggable #= require jquery-ui/effect-highlight #= require jquery-ui/sortable #= require jquery_ujs @@ -31,8 +32,6 @@ #= require ace/ace #= require ace/ext-searchbox #= require underscore -#= require nprogress -#= require nprogress-turbolinks #= require dropzone #= require mousetrap #= require mousetrap/pause @@ -44,6 +43,7 @@ #= require jquery.nicescroll #= require_tree . #= require fuzzaldrin-plus +#= require cropper window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() @@ -109,6 +109,8 @@ window.onload = -> setTimeout shiftWindow, 100 $ -> + bootstrapBreakpoint = bp.getBreakpointSize() + $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") # Click a .js-select-on-focus field, select the contents @@ -138,7 +140,7 @@ $ -> # Initialize tooltips $('body').tooltip( - selector: '.has_tooltip, [data-toggle="tooltip"]' + selector: '.has-tooltip, [data-toggle="tooltip"]' placement: (_, el) -> $el = $(el) $el.data('placement') || 'bottom' @@ -210,82 +212,68 @@ $ -> $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') + $navIconToggle = $('.toggle-nav-collapse') + $(document) .off 'breakpoint:change' .on 'breakpoint:change', (e, breakpoint) -> if breakpoint is 'sm' or breakpoint is 'xs' - $gutterIcon = $('.gutter-toggle').find('i') + $gutterIcon = $sidebarGutterToggle.find('i') if $gutterIcon.hasClass('fa-angle-double-right') - $gutterIcon.closest('a').trigger('click') + $sidebarGutterToggle.trigger('click') + + $navIcon = $navIconToggle.find('.fa') + if $navIcon.hasClass('fa-angle-left') + $navIconToggle.trigger('click') $(document) - .off 'click', 'aside .gutter-toggle' - .on 'click', 'aside .gutter-toggle', (e) -> + .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') - $thisIcon + $allGutterToggleIcons .removeClass('fa-angle-double-right') .addClass('fa-angle-double-left') - $this - .closest('aside') + $('aside.right-sidebar') .removeClass('right-sidebar-expanded') .addClass('right-sidebar-collapsed') $('.page-with-sidebar') .removeClass('right-sidebar-expanded') .addClass('right-sidebar-collapsed') else - $thisIcon + $allGutterToggleIcons .removeClass('fa-angle-double-left') .addClass('fa-angle-double-right') - $this - .closest('aside') + $('aside.right-sidebar') .removeClass('right-sidebar-collapsed') .addClass('right-sidebar-expanded') $('.page-with-sidebar') .removeClass('right-sidebar-collapsed') .addClass('right-sidebar-expanded') - $.cookie("collapsed_gutter", - $('.right-sidebar') - .hasClass('right-sidebar-collapsed'), { path: '/' }) - - bootstrapBreakpoint = undefined; - checkBootstrapBreakpoints = -> - if $('.device-xs').is(':visible') - bootstrapBreakpoint = "xs" - else if $('.device-sm').is(':visible') - bootstrapBreakpoint = "sm" - else if $('.device-md').is(':visible') - bootstrapBreakpoint = "md" - else if $('.device-lg').is(':visible') - bootstrapBreakpoint = "lg" - - setBootstrapBreakpoints = -> - if $('.device-xs').length - return - - $("body") - .append('<div class="device-xs visible-xs"></div>'+ - '<div class="device-sm visible-sm"></div>'+ - '<div class="device-md visible-md"></div>'+ - '<div class="device-lg visible-lg"></div>') - checkBootstrapBreakpoints() + if not triggered + $.cookie("collapsed_gutter", + $('.right-sidebar') + .hasClass('right-sidebar-collapsed'), { path: '/' }) fitSidebarForSize = -> oldBootstrapBreakpoint = bootstrapBreakpoint - checkBootstrapBreakpoints() + 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]) @@ -294,6 +282,5 @@ $ -> .on "resize", (e) -> fitSidebarForSize() - setBootstrapBreakpoints() checkInitialSidebarSize() new Aside() diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee index 85473101944..66ab5054326 100644 --- a/app/assets/javascripts/aside.js.coffee +++ b/app/assets/javascripts/aside.js.coffee @@ -5,7 +5,6 @@ class @Aside e.preventDefault() btn = $(e.currentTarget) icon = btn.find('i') - console.log('1') if icon.hasClass('fa-angle-left') btn.parent().find('section').hide() diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee index 5d3fe81da74..28f8e103664 100644 --- a/app/assets/javascripts/autosave.js.coffee +++ b/app/assets/javascripts/autosave.js.coffee @@ -16,11 +16,11 @@ class @Autosave try text = window.localStorage.getItem @key - catch + catch e return @field.val text if text?.length > 0 - @field.trigger "input" + @field.trigger "input" save: -> return unless window.localStorage? @@ -35,5 +35,5 @@ class @Autosave reset: -> return unless window.localStorage? - try + try window.localStorage.removeItem @key diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 360acb864f6..47b080406d4 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,25 +1,54 @@ class @AwardsHandler constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> - $(".add-award").click (event)-> + $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() - $(".emoji-menu").show() - $("#emoji_search").focus() + + @showEmojiMenu() $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") - $(".emoji-menu").hide() + $(".emoji-menu").removeClass "is-visible" + + $(".awards") + .off "click" + .on "click", ".js-emoji-btn", @handleClick @renderFrequentlyUsedBlock() - @setupSearch() + + handleClick: (e) -> + e.preventDefault() + emoji = $(this) + .find(".icon") + .data "emoji" + awards_handler.addAward emoji + + showEmojiMenu: -> + if $(".emoji-menu").length + if $(".emoji-menu").is ".is-visible" + $(".emoji-menu").removeClass "is-visible" + $("#emoji_search").blur() + else + $(".emoji-menu").addClass "is-visible" + $("#emoji_search").focus() + else + $('.js-add-award').addClass "is-loading" + $.get "/emojis", (response) => + $('.js-add-award').removeClass "is-loading" + $(".js-award-holder").append response + setTimeout => + $(".emoji-menu").addClass "is-visible" + $("#emoji_search").focus() + @setupSearch() + , 200 addAward: (emoji) -> emoji = @normilizeEmojiName(emoji) @postEmoji emoji, => @addAwardToEmojiBar(emoji) - $(".emoji-menu").hide() + $(".emoji-menu").removeClass "is-visible" addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) @@ -29,7 +58,7 @@ class @AwardsHandler if @isActive(emoji) @decrementCounter(emoji) else - counter = @findEmojiIcon(emoji).siblings(".counter") + counter = @findEmojiIcon(emoji).siblings(".js-counter") counter.text(parseInt(counter.text()) + 1) counter.parent().addClass("active") @addMeToAuthorList(emoji) @@ -43,7 +72,7 @@ class @AwardsHandler @findEmojiIcon(emoji).parent().hasClass("active") decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings(".counter") + counter = @findEmojiIcon(emoji).siblings(".js-counter") emojiIcon = counter.parent() if parseInt(counter.text()) > 1 counter.text(parseInt(counter.text()) - 1) @@ -60,9 +89,13 @@ class @AwardsHandler removeMeFromAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() - authors = award_block.attr("data-original-title").split(", ") + authors = award_block + .attr("data-original-title") + .split(", ") authors.splice(authors.indexOf("me"),1) - award_block.closest(".award").attr("data-original-title", authors.join(", ")) + award_block + .closest(".js-emoji-btn") + .attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) addMeToAuthorList: (emoji) -> @@ -88,14 +121,18 @@ class @AwardsHandler emojiCssClass = @resolveNameToCssClass(emoji) nodes = [] - nodes.push("<div class='award active' title='me'>") - nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") - nodes.push("<div class='counter'>1</div>") - nodes.push("</div>") - - emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) - - $(".award").tooltip() + nodes.push( + "<button class='btn award-control js-emoji-btn has-tooltip active' title='me'>", + "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", + "<span class='award-control-text js-counter'>1</span>", + "</button>" + ) + + emoji_node = $(nodes.join("\n")) + .insertBefore(".js-award-holder") + .find(".emoji-icon") + .data("emoji", emoji) + $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") @@ -118,7 +155,7 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".award [data-emoji='#{emoji}']") + $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ @@ -154,13 +191,13 @@ class @AwardsHandler term = $(ev.target).val() # Clean previous search results - $("ul.emoji-search,h5.emoji-search").remove() + $("ul.emoji-menu-search, h5.emoji-search").remove() if term # Generate a search result block h5 = $("<h5>").text("Search results").addClass("emoji-search") found_emojis = @searchEmojis(term).show() - ul = $("<ul>").addClass("emoji-search").append(found_emojis) + 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 diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index 4ec8531d580..6e29d374267 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -1,29 +1,52 @@ # Quick Submit behavior # -# When an input field with the `js-quick-submit` class receives a "Meta+Enter" -# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is -# submitted. +# 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"> -# <input type="text" class="js-quick-submit" /> -# <textarea class="js-quick-submit"></textarea> +# <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 if (e.originalEvent && e.originalEvent.repeat) || e.repeat - return unless e.keyCode == 13 # Enter + return unless keyCodeIs(e, 13) # Enter - if navigator.userAgent.match(/Macintosh/) - return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) - else - return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) + 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') $form.find('input[type=submit], button[type=submit]').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/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee new file mode 100644 index 00000000000..5457430f921 --- /dev/null +++ b/app/assets/javascripts/breakpoints.coffee @@ -0,0 +1,37 @@ +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/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 44d5ddb7d95..7afe8bf79e2 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -4,6 +4,8 @@ class CiBuild constructor: (build_url, build_status) -> clearInterval(CiBuild.interval) + @initScrollButtonAffix() + if build_status == "running" || build_status == "pending" # # Bind autoscroll button to follow build output @@ -38,4 +40,15 @@ class CiBuild 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) + ) + @CiBuild = CiBuild diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee deleted file mode 100644 index 62143e66cfe..00000000000 --- a/app/assets/javascripts/dashboard.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -@Dashboard = - init: -> - $(".projects-list-filter").off('keyup') - this.initSearch() - - initSearch: -> - @timer = null - $(".projects-list-filter").on('keyup', -> - clearTimeout(@timer) - @timer = setTimeout(Dashboard.filterResults, 500) - ) - - 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" diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index b17f8e51470..f5e1ca9860d 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -14,10 +14,7 @@ class Dispatcher path = page.split(':') shortcut_handler = null - switch page - when 'explore:projects:index', 'explore:projects:starred', 'explore:projects:trending' - Dashboard.init() when 'projects:issues:index' Issues.init() shortcut_handler = new ShortcutsNavigation() @@ -25,8 +22,10 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - when 'projects:milestones:show' + 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 DropzoneInput($('.milestone-form')) @@ -59,8 +58,6 @@ class Dispatcher when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() MergeRequests.init() - when 'dashboard:show', 'root:show' - Dashboard.init() when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' @@ -76,8 +73,11 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:show' shortcut_handler = new ShortcutsNavigation() - when 'groups:show' + + new TreeView() if $('#tree-slider').length + when 'groups:activity' new Activities() + when 'groups:show' shortcut_handler = new ShortcutsNavigation() when 'groups:group_members:index' new GroupMembers() @@ -88,10 +88,11 @@ class Dispatcher 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' + when 'projects:blob:show', 'projects:blame:show' new LineHighlighter() shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' @@ -104,9 +105,8 @@ class Dispatcher new ProjectFork() when 'projects:artifacts:browse' new BuildArtifacts() - when 'users:show' - new User() - new Activities() + when 'projects:group_links:index' + new GroupsSelect() switch path.first() when 'admin' diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee new file mode 100644 index 00000000000..df9bfdfa6cc --- /dev/null +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -0,0 +1,152 @@ +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/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee new file mode 100644 index 00000000000..2b56ab2e6de --- /dev/null +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -0,0 +1,331 @@ +class GitLabDropdownFilter + BLUR_KEYCODES = [27, 40] + HAS_VALUE_CLASS = "has-value" + + constructor: (@input, @options) -> + $inputContainer = @input.parent() + $clearButton = $inputContainer.find('.js-dropdown-input-clear') + + # Clear click + $clearButton.on 'click', (e) => + e.preventDefault() + e.stopPropagation() + @input + .val('') + .trigger('keyup') + .focus() + + # Key events + timeout = "" + @input.on "keyup", (e) => + 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 e.keyCode is 13 and @input.val() isnt "" + if @options.enterCallback + @options.enterCallback() + return + + clearTimeout timeout + timeout = setTimeout => + blur_field = @shouldBlur e.keyCode + search_text = @input.val() + + if blur_field + @input.blur() + + if @options.remote + @options.query search_text, (data) => + @options.callback(data) + else + @filter search_text + , 250 + + shouldBlur: (keyCode) -> + return BLUR_KEYCODES.indexOf(keyCode) >= 0 + + filter: (search_text) -> + data = @options.data() + results = data + + if search_text isnt "" + results = fuzzaldrinPlus.filter(data, search_text, + key: @options.keys + ) + + @options.callback results + +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" + + constructor: (@el, @options) -> + self = @ + @dropdown = $(@el).parent() + search_fields = if @options.search then @options.search.fields else []; + + if @options.data + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data + + @parseData @fullData + } + + # Init filiterable + if @options.filterable + @input = @dropdown.find('.dropdown-input .dropdown-input-field') + + @filter = new GitLabDropdownFilter @input, + remote: @options.filterRemote + query: @options.data + keys: @options.search.fields + data: => + return @fullData + callback: (data) => + @parseData data + @highlightRow 1 + enterCallback: => + @selectFirstRow() + + # Event listeners + + @dropdown.on "shown.bs.dropdown", @opened + @dropdown.on "hidden.bs.dropdown", @hidden + @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate + + 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) -> + e.preventDefault() + self.rowClicked $(@) + + if self.options.clicked + self.options.clicked.call(@,e) + + 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 + + parseData: (data) -> + @renderedData = data + + # Render each row + html = $.map data, (obj) => + return @renderItem(obj) + + if @options.filterable and data.length is 0 + # render no matching results + html = [@noResults()] + + # Render the full menu + full_html = @renderMenu(html.join("")) + + @appendMenu(full_html) + + shouldPropagate: (e) => + if @options.multiSelect + $target = $(e.target) + if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') + e.stopPropagation() + return false + else + return true + + opened: => + contentHtml = $('.dropdown-content', @dropdown).html() + if @remote && contentHtml is "" + @remote.execute() + + if @options.filterable + @dropdown.find(".dropdown-input-field").focus() + + hidden: (e) => + if @options.filterable + @dropdown + .find(".dropdown-input-field") + .blur() + .val("") + .trigger("keyup") + + if @dropdown.find(".dropdown-toggle-page").length + $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS + + if @options.hidden + @options.hidden.call(@,e) + + + # Render the full menu + renderMenu: (html) -> + menu_html = "" + + if @options.renderMenu + menu_html = @options.renderMenu(html) + else + menu_html = "<ul>#{html}</ul>" + + 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).html html + + # Render the row + renderItem: (data) -> + html = "" + + return "<li class='divider'></li>" if data is "divider" + + if @options.renderRow + # Call the render function + html = @options.renderRow(data) + else + selected = if @options.isSelected then @options.isSelected(data) else false + 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 + + url = if @options.url then @options.url(data) else "#" + text = if @options.text then @options.text(data) else "" + cssClass = ""; + + if selected + cssClass = "is-active" + + html = "<li>" + html += "<a href='#{url}' class='#{cssClass}'>" + html += text + html += "</a>" + html += "</li>" + + return html + + noResults: -> + html = "<li>" + html += "<a href='#' class='dropdown-menu-empty-link is-focused'>" + html += "No matching results." + html += "</a>" + html += "</li>" + + highlightRow: (index) -> + if @input.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" + + $(selector).addClass 'is-focused' + + rowClicked: (el) -> + fieldName = @options.fieldName + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") + if el.hasClass(ACTIVE_CLASS) + el.removeClass(ACTIVE_CLASS) + field.remove() + else + fieldName = @options.fieldName + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + selectedObject.selected = true + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + + if !value? + field.remove() + + if not @options.multiSelect + @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS + @dropdown.parent().find("input[name='#{fieldName}']").remove() + + # Toggle active class for the tick mark + el.toggleClass "is-active" + + # Toggle the dropdown label + if @options.toggleLabel + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) + if value? + if !field.length + # Create hidden input for form + input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" + if @options.inputId? + input = $(input) + .attr('id', @options.inputId) + @dropdown.before input + + selectFirstRow: -> + selector = '.dropdown-content li:first-child a' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one .dropdown-content li:first-child a" + + # simulate a click on the first link + $(selector).trigger "click" + +$.fn.glDropdown = (opts) -> + return @.each -> + new GitLabDropdown @, opts diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index e52b73f94f6..6fc924d3d66 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -1,8 +1,7 @@ -#= require jquery.waitforimages - class @IssuableContext - constructor: -> - new UsersSelect() + constructor: (currentUser) -> + @initParticipants() + new UsersSelect(currentUser) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $(".issuable-sidebar .inline-update").on "change", "select", -> @@ -11,9 +10,43 @@ class @IssuableContext $(this).submit() $(document).on "click",".edit-link", (e) -> - block = $(@).parents('.block') - block.find('.selectbox').show() - block.find('.value').hide() - block.find('.js-select2').select2("open") + $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.coffee b/app/assets/javascripts/issuable_form.js.coffee index 48c249943f2..7a788f761b7 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -1,4 +1,7 @@ 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() @@ -6,14 +9,17 @@ class @IssuableForm @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", @resetAutosave + @form.on "submit", @handleSubmit @form.on "click", ".btn-cancel", @resetAutosave + @initWip() + initAutosave: -> new Autosave @titleField, [ document.location.pathname, @@ -27,6 +33,50 @@ class @IssuableForm "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()}" diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee new file mode 100644 index 00000000000..c5740f27ddd --- /dev/null +++ b/app/assets/javascripts/issue_status_select.js.coffee @@ -0,0 +1,11 @@ +class @IssueStatusSelect + constructor: -> + $('.js-issue-status').each (i, el) -> + fieldName = $(el).data("field-name") + + $(el).glDropdown( + selectable: true + fieldName: fieldName + id: (obj, el) -> + $(el).data("id") + ) diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index a0acf3028bf..1127b289264 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -41,24 +41,28 @@ @timer = null $("#issue_search").keyup -> clearTimeout(@timer) - @timer = setTimeout(Issues.filterResults, 500) + @timer = setTimeout( -> + Issues.filterResults $("#issue_search_form") + , 500) - filterResults: => - form = $("#issue_search_form") - search = $("#issue_search").val() - $('.issues-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '?' + form.serialize() + filterResults: (form) => + $('.issues-holder, .merge-requests-holder').css("opacity", '0.5') + formAction = form.attr('action') + formData = form.serialize() + issuesUrl = formAction + issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}") + issuesUrl += formData $.ajax type: "GET" - url: form.attr('action') - data: form.serialize() + url: formAction + data: formData complete: -> - $('.issues-holder').css("opacity", '1.0') + $('.issues-holder, .merge-requests-holder').css("opacity", '1.0') success: (data) -> - $('.issues-holder').html(data.html) + $('.issues-holder, .merge-requests-holder').html(data.html) # Change url so if user reload a page - search results are saved - history.replaceState {page: issues_url}, document.title, issues_url + history.replaceState {page: issuesUrl}, document.title, issuesUrl Issues.reload() dataType: "json" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee new file mode 100644 index 00000000000..b5c7af9a8ad --- /dev/null +++ b/app/assets/javascripts/labels_select.js.coffee @@ -0,0 +1,249 @@ +class @LabelsSelect + constructor: -> + $('.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? + 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') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() + + if newLabelField.length + $newLabelCreateButton = $('.js-new-label-btn') + $colorPreview = $('.js-dropdown-label-color-preview') + $newLabelError = $dropdown.parent().find('.js-label-error') + $newLabelError.hide() + + # Suggested colors in the dropdown to chose from pre-chosen colors + $('.suggest-colors-dropdown a').on 'click', (e) -> + + issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? + if issueUpdateURL + labelHTMLTemplate = _.template( + '<% _.each(labels, function(label){ %> + <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> + <span class="label color-label" style="background-color: <%= label.color %>;"> + <%= label.title %> + </span> + </a> + <% }); %>' + ); + labelNoneHTMLTemplate = _.template('<div class="light">None</div>') + + if newLabelField.length and $dropdown.hasClass 'js-extra-options' + $('.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() + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + + $newLabelCreateButton.enable() + else + $newLabelCreateButton.disable() + + 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() + + if newLabelField.val() isnt '' and newColorField.val() isnt '' + $newLabelError.hide() + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + + 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() + $.ajax( + type: 'PUT' + url: issueUpdateURL + dataType: 'JSON' + data: data + ).done (data) -> + $loading.fadeOut() + $selectbox.hide() + data.issueURLSplit = issueURLSplit + if not data.labels.length + template = labelNoneHTMLTemplate() + else + template = labelHTMLTemplate(data) + href = $value + .show() + .html(template) + $value + .find('a') + .each((i) -> + setTimeout(=> + glAnimate($(@), 'pulse') + ,200 * i + ) + ) + + + $dropdown.glDropdown( + data: (term, callback) -> + $.ajax( + url: labelUrl + ).done (data) -> + 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) -> + selectedClass = '' + if $selectbox.find("input[type='hidden']\ + [name='#{$dropdown.data('field-name')}']\ + [value='#{label.id}']").length + selectedClass = 'is-active' + + color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" + + "<li> + <a href='#' class='#{selectedClass}'> + #{color} + #{label.title} + </a> + </li>" + filterable: true + search: + fields: ['title'] + selectable: true + + toggleLabel: (selected) -> + if selected and selected.title isnt 'Any Label' + selected.title + else + defaultLabel + fieldName: $dropdown.data('field-name') + id: (label) -> + if label.isAny? + '' + else if $dropdown.hasClass "js-filter-submit" + label.title + else + label.id + + hidden: -> + $selectbox.hide() + $value.show() + if $dropdown.hasClass 'js-multiselect' + saveLabelData() + + multiSelect: $dropdown.hasClass 'js-multiselect' + + clicked: -> + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is page is 'projects:merge_requests:index' + + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + Issues.filterResults $dropdown.closest('form') + else if $dropdown.hasClass 'js-filter-submit' + $dropdown.closest('form').submit() + else + if $dropdown.hasClass 'js-multiselect' + return + else + saveLabelData() + ) diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee new file mode 100644 index 00000000000..8f892b5a2b9 --- /dev/null +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -0,0 +1,13 @@ +((w) -> + + w.glAnimate = ($el, animation, done) -> + $el + .removeClass() + .addClass(animation + ' animated') + .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> + $(this).removeClass() + return + return + return + +) window
\ No newline at end of file diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index 35b2fbbba07..d14b7139237 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -1,4 +1,4 @@ -NProgress.configure(showSpinner: false) +Turbolinks.enableProgressBar(); defaultClass = 'tanuki-shape' pieces = [ diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee index 98fc8f17340..2a0b9479445 100644 --- a/app/assets/javascripts/markdown_preview.js.coffee +++ b/app/assets/javascripts/markdown_preview.js.coffee @@ -6,6 +6,7 @@ class @MarkdownPreview # Minimum number of users referenced before triggering a warning referenceThreshold: 10 + ajaxCache: {} showPreview: (form) -> preview = form.find('.js-md-preview') @@ -24,12 +25,16 @@ class @MarkdownPreview 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: success + success: (response) => + @ajaxCache = text: text, response: response + success(response) hideReferencedUsers: (form) -> referencedUsers = form.find('.referenced-users') @@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview() previewButtonSelector = '.js-md-preview-button' writeButtonSelector = '.js-md-write-button' +lastTextareaPreviewed = null $.fn.setupMarkdownPreview = -> $form = $(this) @@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = -> form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) form_textarea.on 'blur', -> markdownPreview.showPreview($form) -$(document).on 'click', previewButtonSelector, (e) -> - e.preventDefault() +$(document).on 'markdown-preview:show', (e, $form) -> + return unless $form - $form = $(this).closest('form') + lastTextareaPreviewed = $form.find('textarea.markdown-area') # toggle tabs $form.find(writeButtonSelector).parent().removeClass('active') @@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) -> markdownPreview.showPreview($form) -$(document).on 'click', writeButtonSelector, (e) -> - e.preventDefault() +$(document).on 'markdown-preview:hide', (e, $form) -> + return unless $form - $form = $(this).closest('form') + lastTextareaPreviewed = null # toggle tabs $form.find(writeButtonSelector).parent().addClass('active') @@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) -> # 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_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index b10e1db7f3f..839e6ec2c08 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -3,6 +3,8 @@ # 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"> @@ -68,10 +70,15 @@ class @MergeRequestTabs if action == 'commits' @loadCommits($target.attr('href')) + @expandView() else if action == 'diffs' @loadDiff($target.attr('href')) + @shrinkView() else if action == 'builds' @loadBuilds($target.attr('href')) + @expandView() + else + @expandView() @setCurrentAction(action) @@ -145,7 +152,9 @@ class @MergeRequestTabs url: "#{source}.json" + @_location.search success: (data) => document.querySelector("div#diffs").innerHTML = data.html + $('.js-timeago').timeago() $('div#diffs .js-syntax-highlight').syntaxHighlight() + @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") @@ -177,3 +186,33 @@ class @MergeRequestTabs 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/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index 31f6c6d3d47..0037a3a21c2 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -62,15 +62,24 @@ class @Milestone dataType: "json" 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 + @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) -> data = $(this).sortable("serialize") Milestone.sortIssues(data) @@ -95,11 +104,24 @@ class @Milestone ).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) @@ -123,12 +145,3 @@ class @Milestone Milestone.updateMergeRequest(ui.item, merge_request_url, data) ).disableSelection() - - bindMergeRequestSorting: -> - $('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() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee new file mode 100644 index 00000000000..d1746c38e74 --- /dev/null +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -0,0 +1,107 @@ +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') + 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') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() + + if issueUpdateURL + milestoneLinkTemplate = _.template( + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>' + ) + + milestoneLinkNoneTemplate = '<div class="light">None</div>' + + $dropdown.glDropdown( + data: (term, callback) -> + $.ajax( + url: milestonesUrl + ).done (data) -> + if $dropdown.hasClass "js-extra-options" + if showNo + data.unshift( + id: '0' + title: 'No Milestone' + ) + + if showAny + data.unshift( + isAny: true + title: 'Any Milestone' + ) + + if data.length > 2 + data.splice 2, 0, 'divider' + callback(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) -> + milestone.title + id: (milestone) -> + if !useId + if !milestone.isAny? + milestone.title + else + '' + else + milestone.id + isSelected: (milestone) -> + milestone.title is selectedMilestone + hidden: -> + $selectbox.hide() + $value.show() + clicked: (e) -> + if $dropdown.hasClass 'js-filter-bulk-update' + return + + if $dropdown.hasClass 'js-filter-submit' + $dropdown.parents('form').submit() + else + selected = $selectbox + .find('input[type="hidden"]') + .val() + data = {} + data[abilityName] = {} + data[abilityName].milestone_id = selected + $loading + .fadeIn() + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + ).done (data) -> + $loading.fadeOut() + $selectbox.hide() + $milestoneLink = $value + .show() + .find('a') + if data.milestone? + data.milestone.namespace = _this.currentProject.namespace + data.milestone.path = _this.currentProject.path + $value.html(milestoneLinkTemplate(data.milestone)) + else + $value.html(milestoneLinkNoneTemplate) + )
\ No newline at end of file diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 3347ab65c90..ff06c57f2b5 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -16,11 +16,13 @@ class @Notes @view = view @noteable_url = document.URL @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge") + @basePollingInterval = 15000 + @maxPollingSteps = 4 - @initRefresh() - @setupMainTargetNoteForm() @cleanBinding() @addBinding() + @setPollingInterval() + @setupMainTargetNoteForm() @initTaskList() addBinding: -> @@ -28,8 +30,11 @@ class @Notes $(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 + $(document).on "ajax:success", "form.edit-note", @updateNote # Edit note link $(document).on "click", ".js-note-edit", @showEditForm @@ -37,7 +42,7 @@ class @Notes # Reopen and close actions for Issue/MR combined with note form submit $(document).on "click", ".js-comment-button", @updateCloseButton - $(document).on "keyup", ".js-note-text", @updateTargetButtons + $(document).on "keyup input", ".js-note-text", @updateTargetButtons # remove a note (in general) $(document).on "click", ".js-note-delete", @removeNote @@ -49,6 +54,9 @@ class @Notes $(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 @@ -70,7 +78,7 @@ class @Notes 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 "ajax:success", "form.edit-note" $(document).off "click", ".js-note-edit" $(document).off "click", ".note-edit-cancel" $(document).off "click", ".js-note-delete" @@ -83,6 +91,7 @@ class @Notes $(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" $('.note .js-task-list-container').taskList('disable') $(document).off 'tasklist:changed', '.note .js-task-list-container' @@ -91,9 +100,11 @@ class @Notes clearInterval(Notes.interval) Notes.interval = setInterval => @refresh() - , 15000 + , @pollingInterval refresh: -> + return if @refreshing is true + refreshing = true if not document.hidden and document.URL.indexOf(@noteable_url) is 0 @getContent() @@ -105,12 +116,31 @@ class @Notes success: (data) => notes = data.notes @last_fetched_at = data.last_fetched_at + @setPollingInterval(data.notes.length) $.each notes, (i, note) => if note.discussion_with_diff_html? @renderDiscussionNote(note) else @renderNote(note) + always: => + @refreshing = false + ### + Increase @pollingInterval up to 120 seconds on every function call, + if `shouldReset` has a truthy value, 'null' or 'undefined' the variable + will reset to @basePollingInterval. + + Note: this function is used to gradually increase the polling interval + if there aren't new notes coming from the server + ### + setPollingInterval: (shouldReset = true) -> + nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1) + if shouldReset + @pollingInterval = @basePollingInterval + else if @pollingInterval < nthInterval + @pollingInterval *= 2 + + @initRefresh() ### Render note in main comments area. @@ -196,7 +226,7 @@ class @Notes Resets text and preview. Resets buttons. ### - resetMainTargetForm: -> + resetMainTargetForm: (e) => form = $(".js-main-target-form") # remove validation errors @@ -208,6 +238,8 @@ class @Notes form.find(".js-note-text").data("autosave").reset() + @updateTargetButtons(e) + reenableTargetFormSubmitButton: -> form = $(".js-main-target-form") @@ -251,8 +283,10 @@ class @Notes form.removeClass "js-new-note-form" form.find('.div-dropzone').remove() + # hide discard button + form.find('.js-note-discard').hide() + # setup preview buttons - form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left" previewButton = form.find(".js-md-preview-button") textarea = form.find(".js-note-text") @@ -286,6 +320,10 @@ class @Notes addNote: (xhr, note, status) => @renderNote(note) + addNoteError: (xhr, note, status) => + flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert') + flash.pinTo('.md-area') + ### Called in response to the new note form being submitted @@ -305,6 +343,7 @@ class @Notes updateNote: (_xhr, note, _status) => # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) + $('.js-timeago', $html).timeago() $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') @@ -322,24 +361,26 @@ class @Notes showEditForm: (e) -> e.preventDefault() note = $(this).closest(".note") - note.find(".note-body > .note-text").hide() - note.find(".note-header").hide() - base_form = note.find(".note-edit-form") - form = base_form.clone().insertAfter(base_form) - form.addClass('current-note-edit-form gfm-form') - form.find('.div-dropzone').remove() + note.addClass "is-editting" + form = note.find(".note-edit-form") + isNewForm = form.is(':not(.gfm-form)') + if isNewForm + form.addClass('gfm-form') + form.addClass('current-note-edit-form') # Show the attachment delete link note.find(".js-note-attachment-delete").show() # Setup markdown form - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) + if isNewForm + GitLab.GfmAutoComplete.setup() + new DropzoneInput(form) - form.show() textarea = form.find("textarea") textarea.focus() - autosize(textarea) + + if isNewForm + autosize(textarea) # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # The textarea has the correct value, Chrome just won't show it unless we @@ -348,7 +389,8 @@ class @Notes textarea.val "" textarea.val value - disableButtonIfEmptyField textarea, form.find(".js-comment-button") + if isNewForm + disableButtonIfEmptyField textarea, form.find(".js-comment-button") ### Called in response to clicking the edit note link @@ -358,9 +400,9 @@ class @Notes cancelEdit: (e) -> e.preventDefault() note = $(this).closest(".note") - note.find(".note-body > .note-text").show() - note.find(".note-header").show() - note.find(".current-note-edit-form").remove() + note.removeClass "is-editting" + note.find(".current-note-edit-form") + .removeClass("current-note-edit-form") ### Called in response to deleting a note of any kind. @@ -439,6 +481,11 @@ class @Notes form.find("#note_line_code").val dataHolder.data("lineCode") 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.addClass "js-discussion-note-form" @@ -538,21 +585,52 @@ class @Notes updateCloseButton: (e) => textarea = $(e.target) form = textarea.parents('form') - form.find('.js-note-target-close').text('Close') + 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 - form.find('.js-note-target-reopen').text('Comment & reopen') - form.find('.js-note-target-close').text('Comment & close') - form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen') - form.find('.js-note-target-close').addClass('btn-comment-and-close') + 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 - form.find('.js-note-target-reopen').text('Reopen') - form.find('.js-note-target-close').text('Close') - form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen') - form.find('.js-note-target-close').removeClass('btn-comment-and-close') + 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() diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee index d639303aed3..0ff83b7f0c8 100644 --- a/app/assets/javascripts/pager.js.coffee +++ b/app/assets/javascripts/pager.js.coffee @@ -1,6 +1,7 @@ @Pager = init: (@limit = 0, preload, @disable = false) -> - @loading = $(".loading") + @loading = $('.loading').first() + if preload @offset = 0 @getOld() diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index bb0b66b86e1..ae87c6c4e40 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,26 +1,72 @@ class @Profile - constructor: -> + 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() - $('.update-username form').on 'ajax:before', -> - $('.loading-gif').show() + $('.update-username').on 'ajax:before', -> + $('.loading-username').show() $(this).find('.update-success').hide() $(this).find('.update-failed').hide() - $('.update-username form').on 'ajax:complete', -> + $('.update-username').on 'ajax:complete', -> + $('.loading-username').hide() $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() $('.update-notifications').on 'ajax:complete', -> $(this).find('.btn-save').enable() - $('.js-choose-user-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-user-avatar-input").click() + @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]) + formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png') + + $.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?$/) - $('.js-user-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) + if comment && comment.length > 1 && $title.val() == '' + $title.val(comment[1]).change() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 76bc4ff42a2..87d313ed67c 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -11,7 +11,6 @@ class @Project $(@).toggleClass('active') url = $("#project_clone").val() - console.log("url",url) # Update the input field $('#project_clone').val(url) diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index fecdb9fc2e7..63dee4ed5d7 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -3,3 +3,16 @@ class @ProjectNew $('.project-edit-container').on 'ajax:before', => $('.project-edit-container').hide() $('.save-project-loader').show() + @toggleSettings() + @toggleSettingsOnclick() + + + toggleSettings: -> + checked = $("#project_builds_enabled").prop("checked") + if checked + $('.builds-feature').show() + else + $('.builds-feature').hide() + + toggleSettingsOnclick: -> + $("#project_builds_enabled").on 'click', @toggleSettings diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index eab34be652a..e4c4bf3b273 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -1,26 +1,37 @@ -class @ProjectsList - constructor: -> - $(".projects-list .js-expand").on 'click', (e) -> - e.preventDefault() - list = $(this).closest('.projects-list') +@ProjectsList = + init: -> + $(".projects-list-filter").off('keyup') + this.initSearch() + this.initPagination() - $("#filter_projects").on 'keyup', -> - ProjectsList.filter_results($("#filter_projects")) + initSearch: -> + @timer = null + $(".projects-list-filter").on('keyup', -> + clearTimeout(@timer) + @timer = setTimeout(ProjectsList.filterResults, 500) + ) - @filter_results: ($element) -> - terms = $element.val() - filterSelector = $element.data('filter-selector') || 'span.filter-title' + filterResults: => + $('.projects-list-holder').fadeTo(250, 0.5) - if not terms - $(".projects-list li").show() - $('.gl-pagination').show() - else - $(".projects-list li").each (index) -> - $this = $(this) - name = $this.find(filterSelector).text() + form = null + form = $("form#project-filter-form") + search = $(".projects-list-filter").val() + project_filter_url = form.attr('action') + '?' + form.serialize() - if name.toLowerCase().indexOf(terms.toLowerCase()) == -1 - $this.hide() - else - $this.show() - $('.gl-pagination').hide() + $.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/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index f141fb69c3e..100e3aac535 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -4,17 +4,23 @@ class @Shortcuts Mousetrap.reset() Mousetrap.bind('?', @selectiveHelp) Mousetrap.bind('s', Shortcuts.focusSearch) + Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? selectiveHelp: (e) => Shortcuts.showHelp(e, @enabledHelp) + toggleMarkdownPreview: (e) => + $(document).triggerHandler('markdown-preview:toggle', [e]) + @showHelp: (e, location) -> if $('#modal-shortcuts').length > 0 $('#modal-shortcuts').modal('show') else + url = '/help/shortcuts' + url = gon.relative_url_root + url if gon.relative_url_root? $.ajax( - url: '/help/shortcuts', + url: url, dataType: 'script', success: (e) -> if location and location.length > 0 @@ -33,3 +39,14 @@ $(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_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index cefa1857d7f..bbf02f1db24 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -24,6 +24,10 @@ class @ShortcutsIssuable extends ShortcutsNavigation @nextIssue() return false ) + Mousetrap.bind('e', => + @editIssue() + return false + ) if isMergeRequest @@ -63,3 +67,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation # Focus the input field replyField.focus() + + editIssue: -> + $editBtn = $('.issuable-edit') + Turbolinks.visit($editBtn.attr('href')) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index ae59480af9e..860d4f438d0 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -1,11 +1,26 @@ -$(document).on("click", '.toggle-nav-collapse', (e) -> - e.preventDefault() - collapsed = 'page-sidebar-collapsed' - expanded = 'page-sidebar-expanded' +collapsed = 'page-sidebar-collapsed' +expanded = 'page-sidebar-expanded' +toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) + + setTimeout ( -> + niceScrollBars = $('.nicescroll').niceScroll(); + niceScrollBars.updateScrollBar(); + ), 300 + +$(document).on("click", '.toggle-nav-collapse', (e) -> + e.preventDefault() + + toggleSidebar() ) + +$ -> + size = bp.getBreakpointSize() + + if size is "xs" or size is "sm" + if $('.page-with-sidebar').hasClass(expanded) + toggleSidebar() diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index f5584bcfe4b..31617c88b4a 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -95,4 +95,4 @@ window.ContributorsStatGraphUtil = if date_range is null || date_range[0] <= new Date(date) <= date_range[1] true else - false
\ No newline at end of file + false diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 7f41616d4e7..084f0e0dc65 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -1,17 +1,21 @@ class @Subscription - constructor: (url) -> - $(".subscribe-button").unbind("click").click (event)=> - btn = $(event.currentTarget) - action = btn.find("span").text() - current_status = $(".subscription-status").attr("data-status") - btn.prop("disabled", true) - - $.post url, => - btn.prop("disabled", false) - 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>div").toggleClass("hidden") + constructor: (container) -> + $container = $(container) + @url = $container.attr('data-url') + @subscribe_button = $container.find('.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.prop('disabled', true) + + $.post @url, => + btn.prop('disabled', false) + 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') diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee new file mode 100644 index 00000000000..b6b4bd90e6a --- /dev/null +++ b/app/assets/javascripts/todos.js.coffee @@ -0,0 +1,56 @@ +class @Todos + constructor: (@name) -> + @clearListeners() + @initBtnListeners() + + clearListeners: -> + $('.done-todo').off('click') + $('.js-todos-mark-all').off('click') + + initBtnListeners: -> + $('.done-todo').on('click', @doneClicked) + $('.js-todos-mark-all').on('click', @allDoneClicked) + + 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) => + @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 diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee index ec4271b092c..2882a90d118 100644 --- a/app/assets/javascripts/user.js.coffee +++ b/app/assets/javascripts/user.js.coffee @@ -1,10 +1,17 @@ class @User - constructor: -> + constructor: (@opts) -> $('.profile-groups-avatars').tooltip("placement": "top") - new ProjectsList() + + @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.coffee b/app/assets/javascripts/user_tabs.js.coffee new file mode 100644 index 00000000000..09b7eec9104 --- /dev/null +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -0,0 +1,146 @@ +# 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> +# </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> +# +# <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 .#{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'] + @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 + + 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_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 93c0c7adfee..3ffb18045c1 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,7 +1,191 @@ class @UsersSelect - constructor: -> + 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') + 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') + $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 = selected + $loading + .fadeIn() + $.ajax( + type: 'PUT' + dataType: 'json' + url: issueURL + data: data + ).done (data) -> + $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(noAssigneeTemplate(user)) + $value.find('a').attr('href') + + noAssigneeTemplate = _.template( + '<% if (username) { %> + <a class="author_link " 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="assign-yourself"> + No assignee - + <a href="#" class="js-assign-yourself"> + assign yourself + </a> + </span> + <% } %>' + ) + + $dropdown.glDropdown( + data: (term, callback) => + @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 + selected.name + else + defaultLabel + + inputId: 'issue_assignee_id' + + hidden: (e) -> + $selectbox.hide() + $value.show() + + clicked: -> + 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) + Issues.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') diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 19420f42468..1ee827f1fa3 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -2,7 +2,7 @@ class @Wikis constructor: -> - $('.build-new-wiki').bind 'click', (e) => + $('.new-wiki-page').on 'submit', (e) => $('[data-error~=slug]').addClass('hidden') field = $('#new_wiki_path') slug = @slugify(field.val()) @@ -10,6 +10,7 @@ class @Wikis if (slug.length > 0) path = field.attr('data-wikis-path') location.href = path + '/' + slug + e.preventDefault() dasherize: (value) -> value.replace(/[_\s]+/g, '-') |