diff options
Diffstat (limited to 'app/assets/javascripts/boards')
16 files changed, 907 insertions, 0 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 new file mode 100644 index 00000000000..2c65d4427be --- /dev/null +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -0,0 +1,57 @@ +//= require vue +//= require vue-resource +//= require Sortable +//= require_tree ./models +//= require_tree ./stores +//= require_tree ./services +//= require_tree ./mixins +//= require ./components/board +//= require ./components/new_list_dropdown +//= require ./vue_resource_interceptor + +$(() => { + const $boardApp = document.getElementById('board-app'), + Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + + if (gl.IssueBoardsApp) { + gl.IssueBoardsApp.$destroy(true); + } + + gl.IssueBoardsApp = new Vue({ + el: $boardApp, + components: { + 'board': gl.issueBoards.Board + }, + data: { + state: Store.state, + loading: true, + endpoint: $boardApp.dataset.endpoint, + disabled: $boardApp.dataset.disabled === 'true', + issueLinkBase: $boardApp.dataset.issueLinkBase + }, + init: Store.create.bind(Store), + created () { + gl.boardService = new BoardService(this.endpoint); + }, + ready () { + Store.disabled = this.disabled; + gl.boardService.all() + .then((resp) => { + resp.json().forEach((board) => { + const list = Store.addList(board); + + if (list.type === 'done') { + list.position = Infinity; + } else if (list.type === 'backlog') { + list.position = -1; + } + }); + + Store.addBlankState(); + this.loading = false; + }); + } + }); +}); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 new file mode 100644 index 00000000000..e17784e7948 --- /dev/null +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -0,0 +1,85 @@ +//= require ./board_blank_state +//= require ./board_delete +//= require ./board_list + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.Board = Vue.extend({ + components: { + 'board-list': gl.issueBoards.BoardList, + 'board-delete': gl.issueBoards.BoardDelete, + 'board-blank-state': gl.issueBoards.BoardBlankState + }, + props: { + list: Object, + disabled: Boolean, + issueLinkBase: String + }, + data () { + return { + query: '', + filters: Store.state.filters + }; + }, + watch: { + query () { + this.list.filters = this.getFilterData(); + this.list.getIssues(true); + }, + filters: { + handler () { + this.list.page = 1; + this.list.getIssues(true); + }, + deep: true + } + }, + methods: { + getFilterData () { + const filters = this.filters; + let queryData = { search: this.query }; + + Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; }); + + return queryData; + } + }, + ready () { + const options = gl.issueBoards.getBoardSortableDefaultOptions({ + disabled: this.disabled, + group: 'boards', + draggable: '.is-draggable', + handle: '.js-board-handle', + onEnd: (e) => { + document.body.classList.remove('is-dragging'); + + if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { + const order = this.sortable.toArray(), + $board = this.$parent.$refs.board[e.oldIndex + 1], + list = $board.list; + + $board.$destroy(true); + + this.$nextTick(() => { + Store.state.lists.splice(e.newIndex, 0, list); + Store.moveList(list, order); + }); + } + } + }); + + if (bp.getBreakpointSize() === 'xs') { + options.handle = '.js-board-drag-handle'; + } + + this.sortable = Sortable.create(this.$el.parentNode, options); + }, + beforeDestroy () { + Store.state.lists.$remove(this.list); + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 new file mode 100644 index 00000000000..63d72d857d9 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -0,0 +1,49 @@ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardBlankState = Vue.extend({ + data () { + return { + predefinedLabels: [ + new ListLabel({ title: 'Development', color: '#5CB85C' }), + new ListLabel({ title: 'Testing', color: '#F0AD4E' }), + new ListLabel({ title: 'Production', color: '#FF5F00' }), + new ListLabel({ title: 'Ready', color: '#FF0000' }) + ] + } + }, + methods: { + addDefaultLists () { + this.clearBlankState(); + + this.predefinedLabels.forEach((label, i) => { + Store.addList({ + title: label.title, + position: i, + list_type: 'label', + label: { + title: label.title, + color: label.color + } + }); + }); + + // Save the labels + gl.boardService.generateDefaultLists() + .then((resp) => { + resp.json().forEach((listObj) => { + const list = Store.findList('title', listObj.title); + + list.id = listObj.id; + list.label.id = listObj.label.id; + list.getIssues(); + }); + }); + }, + clearBlankState: Store.removeBlankState.bind(Store) + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 new file mode 100644 index 00000000000..4a7cfeaeab2 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -0,0 +1,43 @@ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardCard = Vue.extend({ + props: { + list: Object, + issue: Object, + issueLinkBase: String, + disabled: Boolean, + index: Number + }, + methods: { + filterByLabel (label, e) { + let labelToggleText = label.title; + const labelIndex = Store.state.filters['label_name'].indexOf(label.title); + $(e.target).tooltip('hide'); + + if (labelIndex === -1) { + Store.state.filters['label_name'].push(label.title); + $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); + } else { + Store.state.filters['label_name'].splice(labelIndex, 1); + labelToggleText = Store.state.filters['label_name'][0]; + $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + } + + const selectedLabels = Store.state.filters['label_name']; + if (selectedLabels.length === 0) { + labelToggleText = 'Label'; + } else if (selectedLabels.length > 1) { + labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; + } + + $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + + Store.updateFiltersUrl(); + } + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 new file mode 100644 index 00000000000..34653cd48ef --- /dev/null +++ b/app/assets/javascripts/boards/components/board_delete.js.es6 @@ -0,0 +1,19 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardDelete = Vue.extend({ + props: { + list: Object + }, + methods: { + deleteBoard () { + $(this.$el).tooltip('hide'); + + if (confirm('Are you sure you want to delete this list?')) { + this.list.destroy(); + } + } + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 new file mode 100644 index 00000000000..1503d14c508 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -0,0 +1,89 @@ +//= require ./board_card + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardList = Vue.extend({ + components: { + 'board-card': gl.issueBoards.BoardCard + }, + props: { + disabled: Boolean, + list: Object, + issues: Array, + loading: Boolean, + issueLinkBase: String + }, + data () { + return { + scrollOffset: 250, + filters: Store.state.filters + }; + }, + watch: { + filters: { + handler () { + this.list.loadingMore = false; + this.$els.list.scrollTop = 0; + }, + deep: true + } + }, + methods: { + listHeight () { + return this.$els.list.getBoundingClientRect().height; + }, + scrollHeight () { + return this.$els.list.scrollHeight; + }, + scrollTop () { + return this.$els.list.scrollTop + this.listHeight(); + }, + loadNextPage () { + const getIssues = this.list.nextPage(); + + if (getIssues) { + this.list.loadingMore = true; + getIssues.then(() => { + this.list.loadingMore = false; + }); + } + }, + }, + ready () { + const options = gl.issueBoards.getBoardSortableDefaultOptions({ + group: 'issues', + sort: false, + disabled: this.disabled, + onStart: (e) => { + const card = this.$refs.issue[e.oldIndex]; + + Store.moving.issue = card.issue; + Store.moving.list = card.list; + }, + onAdd: (e) => { + gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); + }, + onRemove: (e) => { + this.$refs.issue[e.oldIndex].$destroy(true); + } + }); + + if (bp.getBreakpointSize() === 'xs') { + options.handle = '.js-card-drag-handle'; + } + + this.sortable = Sortable.create(this.$els.list, options); + + // Scroll event on list to load more + this.$els.list.onscroll = () => { + if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { + this.loadNextPage(); + } + }; + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 new file mode 100644 index 00000000000..1a4d8157970 --- /dev/null +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -0,0 +1,54 @@ +$(() => { + const Store = gl.issueBoards.BoardsStore; + + $('.js-new-board-list').each(function () { + const $this = $(this); + + new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id')); + + $this.glDropdown({ + data(term, callback) { + $.get($this.attr('data-labels')) + .then((resp) => { + callback(resp); + }); + }, + renderRow (label) { + const active = Store.findList('title', label.title), + $li = $('<li />'), + $a = $('<a />', { + class: (active ? `is-active js-board-list-${active.id}` : ''), + text: label.title, + href: '#' + }), + $labelColor = $('<span />', { + class: 'dropdown-label-box', + style: `background-color: ${label.color}` + }); + + return $li.append($a.prepend($labelColor)); + }, + search: { + fields: ['title'] + }, + filterable: true, + selectable: true, + clicked (label, $el, e) { + e.preventDefault(); + + if (!Store.findList('title', label.title)) { + Store.new({ + title: label.title, + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color + } + }); + } + } + }); + }); +}); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 new file mode 100644 index 00000000000..b7afe4897b6 --- /dev/null +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -0,0 +1,25 @@ +((w) => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { + let defaultSortOptions = { + forceFallback: true, + fallbackClass: 'is-dragging', + fallbackOnBody: true, + ghostClass: 'is-ghost', + filter: '.has-tooltip', + scrollSensitivity: 100, + scrollSpeed: 20, + onStart () { + document.body.classList.add('is-dragging'); + }, + onEnd () { + document.body.classList.remove('is-dragging'); + } + } + + Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); + return defaultSortOptions; + }; +})(window); diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 new file mode 100644 index 00000000000..eb082103de9 --- /dev/null +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -0,0 +1,44 @@ +class ListIssue { + constructor (obj) { + this.id = obj.iid; + this.title = obj.title; + this.confidential = obj.confidential; + this.labels = []; + + if (obj.assignee) { + this.assignee = new ListUser(obj.assignee); + } + + obj.labels.forEach((label) => { + this.labels.push(new ListLabel(label)); + }); + + this.priority = this.labels.reduce((max, label) => { + return (label.priority < max) ? label.priority : max; + }, Infinity); + } + + addLabel (label) { + if (!this.findLabel(label)) { + this.labels.push(new ListLabel(label)); + } + } + + findLabel (findLabel) { + return this.labels.filter( label => label.title === findLabel.title )[0]; + } + + removeLabel (removeLabel) { + if (removeLabel) { + this.labels = this.labels.filter( label => removeLabel.title !== label.title ); + } + } + + removeLabels (labels) { + labels.forEach(this.removeLabel.bind(this)); + } + + getLists () { + return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) ); + } +} diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 new file mode 100644 index 00000000000..e81e91fe972 --- /dev/null +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -0,0 +1,9 @@ +class ListLabel { + constructor (obj) { + this.id = obj.id; + this.title = obj.title; + this.color = obj.color; + this.description = obj.description; + this.priority = (obj.priority !== null) ? obj.priority : Infinity; + } +} diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 new file mode 100644 index 00000000000..be2b8c568a8 --- /dev/null +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -0,0 +1,125 @@ +class List { + constructor (obj) { + this.id = obj.id; + this._uid = this.guid(); + this.position = obj.position; + this.title = obj.title; + this.type = obj.list_type; + this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1; + this.filters = gl.issueBoards.BoardsStore.state.filters; + this.page = 1; + this.loading = true; + this.loadingMore = false; + this.issues = []; + + if (obj.label) { + this.label = new ListLabel(obj.label); + } + + if (this.type !== 'blank' && this.id) { + this.getIssues(); + } + } + + guid() { + const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; + } + + save () { + return gl.boardService.createList(this.label.id) + .then((resp) => { + const data = resp.json(); + + this.id = data.id; + this.type = data.list_type; + this.position = data.position; + + return this.getIssues(); + }); + } + + destroy () { + gl.issueBoards.BoardsStore.state.lists.$remove(this); + gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); + + gl.boardService.destroyList(this.id); + } + + update () { + gl.boardService.updateList(this.id, this.position); + } + + nextPage () { + if (Math.floor(this.issues.length / 20) === this.page) { + this.page++; + + return this.getIssues(false); + } + } + + canSearch () { + return this.type === 'backlog'; + } + + getIssues (emptyIssues = true) { + const filters = this.filters; + let data = { page: this.page }; + + Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); + + if (this.label) { + data.label_name = data.label_name.filter( label => label !== this.label.title ); + } + + if (emptyIssues) { + this.loading = true; + } + + return gl.boardService.getIssuesForList(this.id, data) + .then((resp) => { + const data = resp.json(); + this.loading = false; + + if (emptyIssues) { + this.issues = []; + } + + this.createIssues(data); + }); + } + + createIssues (data) { + data.forEach((issueObj) => { + this.addIssue(new ListIssue(issueObj)); + }); + } + + addIssue (issue, listFrom) { + this.issues.push(issue); + + if (this.label) { + issue.addLabel(this.label); + } + + if (listFrom) { + gl.boardService.moveIssue(issue.id, listFrom.id, this.id); + } + } + + findIssue (id) { + return this.issues.filter( issue => issue.id === id )[0]; + } + + removeIssue (removeIssue) { + this.issues = this.issues.filter((issue) => { + const matchesRemove = removeIssue.id === issue.id; + + if (matchesRemove) { + issue.removeLabel(this.label); + } + + return !matchesRemove; + }); + } +} diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 new file mode 100644 index 00000000000..904b3a68507 --- /dev/null +++ b/app/assets/javascripts/boards/models/user.js.es6 @@ -0,0 +1,8 @@ +class ListUser { + constructor (user) { + this.id = user.id; + this.name = user.name; + this.username = user.username; + this.avatar = user.avatar_url; + } +} diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 new file mode 100644 index 00000000000..9b80fb2e99f --- /dev/null +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -0,0 +1,61 @@ +class BoardService { + constructor (root) { + Vue.http.options.root = root; + + this.lists = Vue.resource(`${root}/lists{/id}`, {}, { + generate: { + method: 'POST', + url: `${root}/lists/generate.json` + } + }); + this.issue = Vue.resource(`${root}/issues{/id}`, {}); + this.issues = Vue.resource(`${root}/lists{/id}/issues`, {}); + + Vue.http.interceptors.push((request, next) => { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + next(); + }); + } + + all () { + return this.lists.get(); + } + + generateDefaultLists () { + return this.lists.generate({}); + } + + createList (label_id) { + return this.lists.save({}, { + list: { + label_id + } + }); + } + + updateList (id, position) { + return this.lists.update({ id }, { + list: { + position + } + }); + } + + destroyList (id) { + return this.lists.delete({ id }); + } + + getIssuesForList (id, filter = {}) { + let data = { id }; + Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); + + return this.issues.get(data); + } + + moveIssue (id, from_list_id, to_list_id) { + return this.issue.update({ id }, { + from_list_id, + to_list_id + }); + } +}; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 new file mode 100644 index 00000000000..18f26a1f911 --- /dev/null +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -0,0 +1,112 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardsStore = { + disabled: false, + state: {}, + moving: { + issue: {}, + list: {} + }, + create () { + this.state.lists = []; + this.state.filters = { + author_id: gl.utils.getParameterValues('author_id')[0], + assignee_id: gl.utils.getParameterValues('assignee_id')[0], + milestone_title: gl.utils.getParameterValues('milestone_title')[0], + label_name: gl.utils.getParameterValues('label_name[]') + }; + }, + addList (listObj) { + const list = new List(listObj); + this.state.lists.push(list); + + return list; + }, + new (listObj) { + const list = this.addList(listObj), + backlogList = this.findList('type', 'backlog', 'backlog'); + + list + .save() + .then(() => { + // Remove any new issues from the backlog + // as they will be visible in the new list + list.issues.forEach(backlogList.removeIssue.bind(backlogList)); + }); + this.removeBlankState(); + }, + updateNewListDropdown (listId) { + $(`.js-board-list-${listId}`).removeClass('is-active'); + }, + shouldAddBlankState () { + // Decide whether to add the blank state + return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]); + }, + addBlankState () { + if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; + + this.addList({ + id: 'blank', + list_type: 'blank', + title: 'Welcome to your Issue Board!', + position: 0 + }); + }, + removeBlankState () { + this.removeList('blank'); + + $.cookie('issue_board_welcome_hidden', 'true', { + expires: 365 * 10 + }); + }, + welcomeIsHidden () { + return $.cookie('issue_board_welcome_hidden') === 'true'; + }, + removeList (id, type = 'blank') { + const list = this.findList('id', id, type); + + if (!list) return; + + this.state.lists = this.state.lists.filter( list => list.id !== id ); + }, + moveList (listFrom, orderLists) { + orderLists.forEach((id, i) => { + const list = this.findList('id', parseInt(id)); + + list.position = i; + }); + listFrom.update(); + }, + moveIssueToList (listFrom, listTo, issue) { + const issueTo = listTo.findIssue(issue.id), + issueLists = issue.getLists(), + listLabels = issueLists.map( listIssue => listIssue.label ); + + // Add to new lists issues if it doesn't already exist + if (!issueTo) { + listTo.addIssue(issue, listFrom); + } + + if (listTo.type === 'done' && listFrom.type !== 'backlog') { + issueLists.forEach((list) => { + list.removeIssue(issue); + }) + issue.removeLabels(listLabels); + } else { + listFrom.removeIssue(issue); + } + }, + findList (key, val, type = 'label') { + return this.state.lists.filter((list) => { + const byType = type ? list['type'] === type : true; + + return list[key] === val && byType; + })[0]; + }, + updateFiltersUrl () { + history.pushState(null, null, `?${$.param(this.state.filters)}`); + } + }; +})(); diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js new file mode 100755 index 00000000000..75f8b730195 --- /dev/null +++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js @@ -0,0 +1,119 @@ +(function () { + 'use strict'; + + function simulateEvent(el, type, options) { + var event; + if (!el) return; + var ownerDocument = el.ownerDocument; + + options = options || {}; + + if (/^mouse/.test(type)) { + event = ownerDocument.createEvent('MouseEvents'); + event.initMouseEvent(type, true, true, ownerDocument.defaultView, + options.button, options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + } else { + event = ownerDocument.createEvent('CustomEvent'); + + event.initCustomEvent(type, true, true, ownerDocument.defaultView, + options.button, options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + + event.dataTransfer = { + data: {}, + + setData: function (type, val) { + this.data[type] = val; + }, + + getData: function (type) { + return this.data[type]; + } + }; + } + + if (el.dispatchEvent) { + el.dispatchEvent(event); + } else if (el.fireEvent) { + el.fireEvent('on' + type, event); + } + + return event; + } + + function getTraget(target) { + var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + var children = el.children; + + return ( + children[target.index] || + children[target.index === 'first' ? 0 : -1] || + children[target.index === 'last' ? children.length - 1 : -1] + ); + } + + function getRect(el) { + var rect = el.getBoundingClientRect(); + var width = rect.right - rect.left; + var height = rect.bottom - rect.top; + + return { + x: rect.left, + y: rect.top, + cx: rect.left + width / 2, + cy: rect.top + height / 2, + w: width, + h: height, + hw: width / 2, + wh: height / 2 + }; + } + + function simulateDrag(options, callback) { + options.to.el = options.to.el || options.from.el; + + var fromEl = getTraget(options.from); + var toEl = getTraget(options.to); + var scrollable = options.scrollable; + + var fromRect = getRect(fromEl); + var toRect = getRect(toEl); + + var startTime = new Date().getTime(); + var duration = options.duration || 1000; + simulateEvent(fromEl, 'mousedown', {button: 0}); + options.ontap && options.ontap(); + window.SIMULATE_DRAG_ACTIVE = 1; + + var dragInterval = setInterval(function loop() { + var progress = (new Date().getTime() - startTime) / duration; + var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; + var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop; + var overEl = fromEl.ownerDocument.elementFromPoint(x, y); + + simulateEvent(overEl, 'mousemove', { + clientX: x, + clientY: y + }); + + if (progress >= 1) { + options.ondragend && options.ondragend(); + simulateEvent(toEl, 'mouseup'); + clearInterval(dragInterval); + window.SIMULATE_DRAG_ACTIVE = 0; + } + }, 100); + + return { + target: fromEl, + fromList: fromEl.parentNode, + toList: toEl.parentNode + }; + } + + + // Export + window.simulateEvent = simulateEvent; + window.simulateDrag = simulateDrag; +})(); diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..66f645a4b61 --- /dev/null +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -0,0 +1,8 @@ +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + setTimeout(() => { + Vue.activeResources--; + }, 500); + next(); +}); |