diff --git a/src/App.vue b/src/App.vue
index dd984145b..9262ec871 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1349,4 +1349,48 @@ progress::-moz-progress-bar
100%
opacity 1
+// Item Snap Guides
+
+// waiting animations
+
+@keyframes guideRightWaiting
+ 0%
+ opacity 0
+ 100%
+ transform translateX(2px)
+ opacity 0.75
+@keyframes guideLeftWaiting
+ 0%
+ opacity 0
+ 100%
+ transform translateX(-2px)
+ opacity 0.75
+@keyframes guideTopWaiting
+ 0%
+ opacity 0
+ 100%
+ transform translateY(-2px)
+ opacity 0.75
+@keyframes guideBottomWaiting
+ 0%
+ opacity 0
+ 100%
+ transform translateY(2px)
+ opacity 0.75
+
+// ready animations
+
+@keyframes guideRightReady
+ 50%
+ transform translateX(2px)
+@keyframes guideLeftReady
+ 50%
+ transform translateX(-2px)
+@keyframes guideTopReady
+ 50%
+ transform translateY(-2px)
+@keyframes guideBottomReady
+ 50%
+ transform translateY(2px)
+
diff --git a/src/components/Box.vue b/src/components/Box.vue
index ed809263b..1e0482c69 100644
--- a/src/components/Box.vue
+++ b/src/components/Box.vue
@@ -2,6 +2,7 @@
import { reactive, computed, onMounted, onBeforeUnmount, onUpdated, onUnmounted, defineProps, defineEmits, watch, ref, nextTick } from 'vue'
import { useStore } from 'vuex'
+import BoxSnapGuide from '@/components/BoxSnapGuide.vue'
import utils from '@/utils.js'
import consts from '@/consts.js'
import fonts from '@/data/fonts.js'
@@ -863,6 +864,7 @@ const isInCheckedBox = computed(() => {
tabindex="-1"
)
img.resize-icon.icon(src="@/assets/resize-corner.svg" :class="resizeColorClass")
+ BoxSnapGuide(:box="props.box")
diff --git a/src/components/Boxes.vue b/src/components/Boxes.vue
index cc6827bb2..2dde22229 100644
--- a/src/components/Boxes.vue
+++ b/src/components/Boxes.vue
@@ -3,7 +3,6 @@ import { reactive, computed, onMounted, onBeforeUnmount, defineProps, defineEmit
import { useStore } from 'vuex'
import Box from '@/components/Box.vue'
-import BoxSnapGuide from '@/components/BoxSnapGuide.vue'
const store = useStore()
const isPainting = computed(() => store.state.currentUserIsPainting)
diff --git a/src/components/Card.vue b/src/components/Card.vue
index 5bc19ed70..02fee911a 100644
--- a/src/components/Card.vue
+++ b/src/components/Card.vue
@@ -11,6 +11,7 @@ import UserLabelInline from '@/components/UserLabelInline.vue'
import OtherCardPreview from '@/components/OtherCardPreview.vue'
import GroupInvitePreview from '@/components/GroupInvitePreview.vue'
import ItemConnectorButton from '@/components/ItemConnectorButton.vue'
+import CardSnapGuide from '@/components/CardSnapGuide.vue'
import consts from '@/consts.js'
import postMessage from '@/postMessage.js'
@@ -2101,6 +2102,7 @@ article.card-wrap#card(
img.icon.time(src="@/assets/time.svg")
.name {{dateUpdatedAt}}
+ CardSnapGuide(:card="card")
diff --git a/src/components/CardsCreatedProgress.vue b/src/components/CardsCreatedProgress.vue
index d504b382d..23267465e 100644
--- a/src/components/CardsCreatedProgress.vue
+++ b/src/components/CardsCreatedProgress.vue
@@ -18,7 +18,7 @@ const state = reactive({
freeLimitFAQIsVisible: false
})
-const cardsCreatedCount = computed(() => store.state.currentUser.cardsCreatedCount || 0)
+const cardsCreatedCount = computed(() => Math.min(0, store.state.currentUser.cardsCreatedCount || 0))
const cardsCreatedLimit = computed(() => store.state.cardsCreatedLimit)
const triggerUpgradeUserIsVisible = () => {
diff --git a/src/components/dialogs/BackgroundPicker.vue b/src/components/dialogs/BackgroundPicker.vue
index 528527f87..30e7cb346 100644
--- a/src/components/dialogs/BackgroundPicker.vue
+++ b/src/components/dialogs/BackgroundPicker.vue
@@ -484,7 +484,7 @@ dialog.background-picker.wide(v-if="visible" :open="visible" @click.left.stop="c
button.change-color(@click.left.stop="toggleColorPicker" :class="{active: state.colorPickerIsVisible}")
span.current-color(:style="{ background: backgroundTintBadgeColor }")
span Tint
- ColorPicker(:currentColor="state.backgroundTint || '#fff'" :visible="state.colorPickerIsVisible" @selectedColor="updateBackgroundTint" :removeIsVisible="true" @removeColor="removeBackgroundTint" :shouldLightenColors="true")
+ ColorPicker(:currentColor="state.backgroundTint || '#fff'" :visible="state.colorPickerIsVisible" @selectedColor="updateBackgroundTint" :removeIsVisible="true" @removeColor="removeBackgroundTint" :shouldLightenColors="true" :luminosityIsLight="true")
//- Type
.segmented-buttons
button(@click.left.stop="updateService('background')" :class="{ active: state.service === 'background'}")
diff --git a/src/components/dialogs/ColorPicker.vue b/src/components/dialogs/ColorPicker.vue
index 587964179..2f7ac0c5d 100644
--- a/src/components/dialogs/ColorPicker.vue
+++ b/src/components/dialogs/ColorPicker.vue
@@ -26,7 +26,8 @@ const props = defineProps({
removeIsVisible: Boolean,
shouldLightenColors: Boolean,
recentColors: Array,
- luminosityIsDark: Boolean
+ luminosityIsDark: Boolean,
+ luminosityIsLight: Boolean
})
watch(() => props.visible, (value, prevValue) => {
if (value) {
@@ -130,6 +131,10 @@ const updateLuminosityFromTheme = () => {
updateLuminosity('dark')
return
}
+ if (props.luminosityIsLight) {
+ updateLuminosity('light')
+ return
+ }
const isThemeDark = store.state.currentUser.theme === 'dark'
if (isThemeDark) {
updateLuminosity('dark')
diff --git a/src/consts.js b/src/consts.js
index 616c30898..a24980b42 100644
--- a/src/consts.js
+++ b/src/consts.js
@@ -19,7 +19,7 @@ export default {
emptyCard () {
return { width: this.defaultCardWidth, height: 32 }
},
- boxSnapGuideWaitingDuration: 600,
+ boxSnapGuideWaitingDuration: 500,
maxInviteEmailsAllowedToSend: 15,
defaultConnectionPathCurveControlPoint: 'q90,40',
defaultTimeout: 40000,
diff --git a/src/data/backgroundImages.json b/src/data/backgroundImages.json
index 73b904439..234f6cf3b 100644
--- a/src/data/backgroundImages.json
+++ b/src/data/backgroundImages.json
@@ -8,7 +8,7 @@
},
{
"url": "https://bk.kinopio.club/grid-large-boxes-2x.png",
- "darkUrl": "https://bk.kinopio.club/grid-large-boxes-dark-2x.png",
+ "darkUrl": "https://bk.kinopio.club/grid-large-boxes-dark-3-2x.png",
"previewUrl": "https://bk.kinopio.club/grid-large-boxes-thumbnail.png",
"thumbnailUrl": "https://bk.kinopio.club/grid-large-boxes-thumbnail-5.svg"
},
diff --git a/src/store/currentBoxes.js b/src/store/currentBoxes.js
index 4035a7516..67663d635 100644
--- a/src/store/currentBoxes.js
+++ b/src/store/currentBoxes.js
@@ -1,4 +1,3 @@
-// import utils from '@/utils.js'
import cache from '@/cache.js'
import utils from '@/utils.js'
import consts from '@/consts.js'
@@ -307,7 +306,7 @@ export default {
updateSnapGuides: (context, { boxes, cards }) => {
if (context.rootState.shouldSnapToGrid) { return }
- const snapThreshold = 6
+ const snapThreshold = 10
const spaceEdgeThreshold = 100
let targetBoxes = utils.clone(context.getters.isSelectableInViewport)
const prevSnapGuides = context.state.snapGuides
@@ -332,13 +331,13 @@ export default {
targetBox.height = targetBox.resizeHeight
const isBetweenTargetBoxPointsX = utils.isBetween({
value: item.x,
- min: targetBox.x + snapThreshold,
- max: targetBox.x + targetBox.width - snapThreshold
+ min: targetBox.x - snapThreshold,
+ max: targetBox.x + targetBox.width + snapThreshold
})
const isBetweenTargetBoxPointsY = utils.isBetween({
value: item.y,
- min: targetBox.y + snapThreshold,
- max: targetBox.y + targetBox.height - snapThreshold
+ min: targetBox.y - snapThreshold,
+ max: targetBox.y + targetBox.height + snapThreshold
})
// let time = 1
// item sides
diff --git a/src/store/currentCards.js b/src/store/currentCards.js
index 2f572e95e..b62d119d0 100644
--- a/src/store/currentCards.js
+++ b/src/store/currentCards.js
@@ -38,7 +38,8 @@ const currentCards = {
ids: [],
cards: {}, // {id, {card}}
removedCards: [], // denormalized
- tallestCardHeight: 0
+ tallestCardHeight: 0,
+ snapGuide: null // { side, origin, target }, { ... }
},
mutations: {
@@ -157,6 +158,9 @@ const currentCards = {
element.dataset.y = card.y
})
},
+ snapGuide: (state, value) => {
+ state.snapGuide = value
+ },
// broadcast
@@ -238,6 +242,8 @@ const currentCards = {
card.spaceId = currentSpaceId
card.isComment = isComment
card.shouldShowOtherSpacePreviewImage = true
+ card.listId = null
+ card.listRank = null
// create card
context.commit('cardDetailsIsVisibleForCardId', card.id, { root: true })
context.dispatch('broadcast/update', { updates: { card }, type: 'createCard', handler: 'currentCards/create' }, { root: true })
@@ -675,6 +681,7 @@ const currentCards = {
context.dispatch('currentConnections/updatePathsWhileDragging', { connections }, { root: true })
context.dispatch('currentBoxes/updateSnapGuides', { cards }, { root: true })
context.dispatch('broadcast/update', { updates: { cards }, type: 'moveCards', handler: 'currentCards/moveWhileDragging' }, { root: true })
+ context.dispatch('updateSnapGuide')
},
checkIfShouldIncreasePageWidthWhileDragging: (context, card) => {
const cardEnd = card.x + card.width
@@ -732,6 +739,68 @@ const currentCards = {
})
},
+ // snapping
+
+ updateSnapGuide: (context) => {
+ const cardId = context.rootState.currentDraggingCardId
+ if (!cardId) { return }
+ const snapThreshold = 10
+ const spaceEdgeThreshold = 100
+ let targetCards = context.getters.isSelectableInViewport
+ let newSnapGuide = null
+ const card = utils.cardElementDimensions({ id: cardId })
+ targetCards.find(targetCard => {
+ if (targetCard.id === card.id) { return }
+ const isBetweenTargetCardPointsX = utils.isBetween({
+ value: card.x,
+ min: targetCard.x - snapThreshold,
+ max: targetCard.x + targetCard.width + snapThreshold
+ })
+ // card sides
+ const cardTop = card.y
+ const cardBottom = card.y + card.height
+ // target sides
+ const targetCardTop = targetCard.y
+ const targetCardBottom = targetCard.y + targetCard.height
+ // snap top
+ const isSnapTopFromCardBottom = Math.abs(cardBottom - targetCardTop) <= snapThreshold
+ if (isBetweenTargetCardPointsX && isSnapTopFromCardBottom) {
+ newSnapGuide = context.getters.newSnapGuide({ side: 'top', card, targetCard })
+ newSnapGuide.color = context.rootState.currentLists.newListColor
+ }
+ // snap bottom
+ const isSnapBottomFromCardTop = Math.abs(cardTop - targetCardBottom) <= snapThreshold
+ if (isBetweenTargetCardPointsX && isSnapBottomFromCardTop) {
+ newSnapGuide = context.getters.newSnapGuide({ side: 'bottom', card, targetCard })
+ newSnapGuide.color = context.rootState.currentLists.newListColor
+ }
+ return newSnapGuide
+ })
+ context.commit('snapGuide', newSnapGuide)
+ },
+ snap: (context, { snapGuide }) => {
+ if (context.rootState.cardDetailsIsVisibleForCardId) { return }
+ const cards = context.getters.isSelected
+ const target = context.getters.byId(snapGuide.target.id)
+ console.log('🪲🪲🪲🪲🪲', snapGuide.side, snapGuide.target, target, '🐻❄️', cards)
+ context.commit('currentCards/snapGuide', null, { root: true })
+
+ // TODO create new module currentLists
+ // lists getter isCardInList card.listId?
+ // getter listById
+ // getter cardsByListId
+
+ // if target card is not in a list then create a new list w targetCard + list
+
+ // dispatch currentLists/new , ordered cards
+ // update card.listId
+ // cards .listRank (lexorank)
+
+ // dispatch currentLists/addCards (target, cards)
+ // if target is in list
+ // append or prepend to existing list
+ },
+
// distribute position
distributeVertically: async (context, cards) => {
@@ -1136,6 +1205,14 @@ const currentCards = {
shouldSnapToGrid: (state, getters, rootState, rootGetters) => {
if (rootState.currentDraggingBoxId) { return }
return rootState.shouldSnapToGrid
+ },
+ newSnapGuide: (state) => ({ side, card, targetCard }) => {
+ let time = Date.now()
+ const prevGuide = state.snapGuide
+ if (prevGuide) {
+ time = prevGuide.time
+ }
+ return { side, origin: card, target: targetCard, time }
}
}
}
diff --git a/src/store/currentLists.js b/src/store/currentLists.js
new file mode 100644
index 000000000..6b6a28683
--- /dev/null
+++ b/src/store/currentLists.js
@@ -0,0 +1,485 @@
+import cache from '@/cache.js'
+import utils from '@/utils.js'
+import consts from '@/consts.js'
+
+import { nanoid } from 'nanoid'
+import randomColor from 'randomcolor'
+import uniq from 'lodash-es/uniq'
+import { nextTick } from 'vue'
+
+// normalized state
+// https://github.com/vuejs/vuejs.org/issues/1636
+
+let currentSpaceId
+let prevMovePositions = {}
+
+export default {
+ namespaced: true,
+ state: {
+ ids: [],
+ lists: {},
+ newListColor: randomColor({ luminosity: 'dark' })
+ },
+ mutations: {
+
+ // init
+
+ clear: (state) => {
+ state.ids = []
+ state.lists = {}
+ },
+ restore: (state, lists) => {
+ let ids = []
+ lists.forEach(list => {
+ ids.push(list.id)
+ state.lists[list.id] = list
+ })
+ state.ids = state.ids.concat(ids)
+ },
+
+ // create
+
+ create: (state, list) => {
+ state.ids.push(list.id)
+ state.lists[list.id] = list
+ cache.updateSpace('lists', state.lists, currentSpaceId)
+ },
+
+ // update
+
+ update: (state, list) => {
+ const keys = Object.keys(list)
+ keys.forEach(key => {
+ state.lists[list.id][key] = list[key]
+ })
+ cache.updateSpace('lists', state.lists, currentSpaceId)
+ },
+ move: (state, { lists, spaceId }) => {
+ lists.forEach(list => {
+ state.lists[list.id].x = list.x
+ state.lists[list.id].y = list.y
+ })
+ cache.updateSpaceListsDebounced(state.lists, currentSpaceId)
+ },
+ snapGuides: (state, value) => {
+ state.snapGuides = value
+ },
+ resizeWhileDragging: (state, { lists, shouldSnapToGrid }) => {
+ lists.forEach(list => {
+ const element = utils.listElementFromId(list.id)
+ if (!element) { return }
+ if (element.dataset.isVisibleInViewport === 'false') { return }
+ if (shouldSnapToGrid) {
+ element.style.width = utils.roundToNearest(list.resizeWidth) + 'px'
+ element.style.height = utils.roundToNearest(list.resizeHeight) + 'px'
+ } else {
+ element.style.width = list.resizeWidth + 'px'
+ element.style.height = list.resizeHeight + 'px'
+ }
+ element.dataset.resizeWidth = list.resizeWidth
+ element.dataset.resizeHeight = list.resizeHeight
+ })
+ },
+ moveWhileDragging: (state, { lists }) => {
+ lists.forEach(list => {
+ const element = document.querySelector(`.list[data-list-id="${list.id}"]`)
+ if (!element) { return }
+ if (element.dataset.isVisibleInViewport !== 'false') {
+ element.style.left = list.x + 'px'
+ element.style.top = list.y + 'px'
+ }
+ element.dataset.x = list.x
+ element.dataset.y = list.y
+ })
+ },
+
+ // broadcast
+
+ moveBroadcast: (state, { lists }) => {
+ lists.forEach(updated => {
+ const list = state.lists[updated.id]
+ if (!list) { return }
+ list.x = updated.x
+ list.y = updated.y
+ })
+ cache.updateSpaceListsDebounced(state.lists, currentSpaceId)
+ },
+
+ // remove
+
+ remove: (state, listToRemove) => {
+ if (!listToRemove) { return }
+ const list = state.lists[listToRemove.id]
+ if (!list) { return }
+ state.ids = state.ids.filter(id => id !== list.id)
+ delete state.lists[list.id]
+ cache.updateSpace('lists', state.lists, currentSpaceId)
+ }
+ },
+ actions: {
+
+ // init
+
+ updateSpaceId: (context, spaceId) => {
+ currentSpaceId = spaceId
+ },
+ mergeUnique: (context, { newItems, itemType }) => {
+ newItems.forEach(newdList => {
+ let shouldUpdate
+ let prevdList = context.getters.byId(newdList.id)
+ let list = { id: newdList.id }
+ let keys = Object.keys(newdList)
+ keys = keys.filter(key => key !== 'id')
+ keys.forEach(key => {
+ if (prevdList[key] !== newdList[key]) {
+ list[key] = newdList[key]
+ shouldUpdate = true
+ }
+ })
+ if (!shouldUpdate) { return }
+ context.commit('update', list)
+ })
+ },
+ mergeRemove: (context, { removeItems, itemType }) => {
+ removeItems.forEach(list => {
+ context.commit('remove', list)
+ })
+ },
+
+ // create
+
+ add: async (context, { list, shouldResize }) => {
+ const count = context.state.ids.length
+ const mindListSize = consts.mindListSize
+ const isThemeDark = context.rootState.currentUser.theme === 'dark'
+ const color = randomColor({ luminosity: 'dark' })
+ list = {
+ id: list.id || nanoid(),
+ spaceId: currentSpaceId,
+ userId: context.rootState.currentUser.id,
+ x: list.x,
+ y: list.y,
+ resizeWidth: list.resizeWidth || mindListSize,
+ resizeHeight: list.resizeHeight || mindListSize,
+ color: list.color || color,
+ fill: list.fill || 'filled', // empty, filled
+ name: list.name || `dList ${count}`,
+ infoHeight: 57,
+ infoWidth: 34,
+ headerFontId: context.rootState.currentUser.prevHeaderFontId || 0
+ }
+ context.dispatch('history/add', { lists: [list] }, { root: true })
+ context.commit('create', list)
+ context.dispatch('broadcast/update', { updates: list, type: 'createdList', handler: 'currentLists/create' }, { root: true })
+ if (shouldResize) {
+ context.dispatch('history/pause', null, { root: true })
+ context.commit('currentUserIsResizingdList', true, { root: true })
+ context.commit('currentUserIsResizingdListIds', [list.id], { root: true })
+ }
+ await context.dispatch('api/addToQueue', { name: 'createdList', body: list }, { root: true })
+ },
+
+ // update
+
+ update: async (context, list) => {
+ context.dispatch('history/add', { lists: [list] }, { root: true })
+ context.commit('update', list)
+ context.dispatch('broadcast/update', { updates: list, type: 'updatedList', handler: 'currentLists/update' }, { root: true })
+ const keys = Object.keys(list)
+ const shouldUpdatePathsKeys = ['x', 'resizeWidth']
+ let shouldUpdatePaths = keys.find(key => shouldUpdatePathsKeys.includes(key))
+ if (shouldUpdatePaths) {
+ nextTick(() => {
+ context.dispatch('currentConnections/updatePaths', { itemId: list.id }, { root: true })
+ })
+ }
+ await context.dispatch('api/addToQueue', { name: 'updatedList', body: list }, { root: true })
+ },
+ updateName (context, { list, newName }) {
+ const canEditdList = context.rootGetters['currentUser/canEditdList'](list)
+ if (!canEditdList) { return }
+ context.dispatch('update', {
+ id: list.id,
+ name: newName
+ })
+ },
+ updateMultiple: async (context, lists) => {
+ const spaceId = context.rootState.currentSpace.id
+ let updates = {
+ lists,
+ spaceId: context.rootState.currentSpace.id
+ }
+ updates.lists.map(list => {
+ delete list.userId
+ return list
+ })
+ context.dispatch('history/add', { lists }, { root: true })
+ lists.forEach(list => {
+ context.dispatch('broadcast/update', { updates: list, type: 'updatedList', handler: 'currentLists/update' }, { root: true })
+ context.commit('update', list)
+ })
+ cache.updateSpace('editedByUserId', context.rootState.currentUser.id, currentSpaceId)
+ await context.dispatch('api/addToQueue', { name: 'updateMultipleLists', body: updates }, { root: true })
+ },
+
+ // checklists
+
+ // toggleChecked (context, { listId, value }) {
+ // utils.typeCheck({ value, type: 'boolean' })
+ // utils.typeCheck({ value: listId, type: 'string' })
+ // const list = context.getters.byId(listId)
+ // let name = list.name
+ // const checklist = utils.checklistFromString(name)
+ // name = name.replace(checklist, '')
+ // if (value) {
+ // name = `[x] ${name}`
+ // } else {
+ // name = `[] ${name}`
+ // }
+ // const update = {
+ // id: listId,
+ // name
+ // }
+ // context.dispatch('update', update)
+ // },
+ // removeChecked: (context, listId) => {
+ // utils.typeCheck({ value: listId, type: 'string' })
+ // const list = context.getters.byId(listId)
+ // let name = list.name
+ // name = name.replace('[x]', '').trim()
+ // const update = {
+ // id: listId,
+ // name
+ // }
+ // context.dispatch('update', update)
+ // },
+
+ // resize
+
+ resize: (context, { listIds, delta }) => {
+ let connections = []
+ let lists = []
+ listIds.forEach(listId => {
+ const rect = utils.listElementDimensions({ id: listId })
+ let width = rect.width
+ let height = rect.height
+ width = width + delta.x
+ height = height + delta.y
+ const list = { id: listId, resizeWidth: width, resizeHeight: height }
+ lists.push(list)
+ connections = connections.concat(context.rootGetters['currentConnections/byItemId'](list.id))
+ context.commit('currentUserIsResizingdList', true, { root: true })
+ context.commit('currentUserIsResizingdListIds', [list.id], { root: true })
+ })
+ context.commit('resizeWhileDragging', { lists, shouldSnapToGrid: context.rootState.shouldSnapToGrid })
+ context.dispatch('currentConnections/updatePathsWhileDragging', { connections }, { root: true })
+ context.dispatch('broadcast/update', { updates: { lists }, type: 'resizeLists', handler: 'currentLists/resizeWhileDragging' }, { root: true })
+ },
+
+ // dimensions
+
+ updateInfoDimensions: async (context, { lists }) => {
+ lists = lists || utils.clone(context.getters.all)
+ for (const list of lists) {
+ const prevDimensions = {
+ infoWidth: list.infoWidth,
+ infoHeight: list.infoHeight
+ }
+ const element = document.querySelector(`.list-info[data-list-id="${list.id}"]`)
+ if (!element) { return }
+ const DOMRect = element.getBoundingClientRect()
+ const infoWidth = Math.round(DOMRect.width + 4)
+ const infoHeight = Math.round(DOMRect.height)
+ const dimensionsChanged = infoWidth !== prevDimensions.infoWidth || infoHeight !== prevDimensions.infoHeight
+ const body = {
+ id: list.id,
+ infoWidth,
+ infoHeight
+ }
+ if (!dimensionsChanged) { return }
+ context.commit('update', body)
+ await context.dispatch('api/addToQueue', { name: 'updatedList', body }, { root: true })
+ }
+ },
+
+ // move
+
+ move: (context, { endCursor, prevCursor, delta }) => {
+ const zoom = context.rootGetters.spaceCounterZoomDecimal
+ if (!endCursor || !prevCursor) { return }
+ endCursor = {
+ x: endCursor.x * zoom,
+ y: endCursor.y * zoom
+ }
+ if (context.rootState.shouldSnapToGrid) {
+ prevCursor = utils.cursorPositionSnapToGrid(prevCursor)
+ endCursor = utils.cursorPositionSnapToGrid(endCursor)
+ }
+ delta = delta || {
+ x: endCursor.x - prevCursor.x,
+ y: endCursor.y - prevCursor.y
+ }
+ let lists = context.getters.isSelected
+ if (!lists.length) { return }
+ lists = lists.filter(list => context.rootGetters['currentUser/canEditdList'](list))
+ // prevent lists bunching up at 0
+ let connections = []
+ lists.forEach(list => {
+ if (!list) { return }
+ if (!list.x) { list.y = 0 }
+ if (!list.y) { list.y = 0 }
+ if (list.x === 0) { delta.x = Math.max(0, delta.x) }
+ if (list.y === 0) { delta.y = Math.max(0, delta.y) }
+ connections = connections.concat(context.rootGetters['currentConnections/byItemId'](list.id))
+ })
+ lists = lists.filter(list => Boolean(list))
+ // prevent lists with null or negative positions
+ lists = utils.clone(lists)
+ lists = lists.map(list => {
+ let position
+ if (prevMovePositions[list.id]) {
+ position = prevMovePositions[list.id]
+ } else {
+ position = utils.listPositionFromElement(list.id)
+ }
+ list.x = position.x
+ list.y = position.y
+ // x
+ if (list.x === undefined || list.x === null) {
+ delete list.x
+ } else {
+ list.x = Math.max(0, list.x + delta.x)
+ list.x = Math.round(list.x)
+ }
+ // y
+ if (list.y === undefined || list.y === null) {
+ delete list.y
+ } else {
+ list.y = Math.max(0, list.y + delta.y)
+ list.y = Math.round(list.y)
+ list.y = Math.max(consts.minItemXY, list.y)
+ }
+ list = {
+ name: list.name,
+ x: list.x,
+ y: list.y,
+ width: list.resizeWidth,
+ height: list.resizeHeight,
+ id: list.id
+ }
+ prevMovePositions[list.id] = list
+ return list
+ })
+ // update
+ context.commit('moveWhileDragging', { lists })
+ context.commit('listsWereDragged', true, { root: true })
+ context.dispatch('currentConnections/updatePathsWhileDragging', { connections }, { root: true })
+ context.dispatch('broadcast/update', { updates: { lists }, type: 'moveLists', handler: 'currentLists/moveWhileDragging' }, { root: true })
+ context.dispatch('updateSnapGuides', { lists })
+ },
+ afterMove: (context) => {
+ prevMovePositions = {}
+ const currentDraggingdListId = context.rootState.currentDraggingdListId
+ const currentDraggingdList = context.getters.byId(currentDraggingdListId)
+ const spaceId = context.rootState.currentSpace.id
+ let listIds = context.getters.isSelectedIds
+ listIds = listIds.filter(list => Boolean(list))
+ if (!listIds.length) { return }
+ // lists
+ let lists = listIds.map(id => {
+ let list = context.getters.byId(id)
+ if (!list) { return }
+ list = utils.clone(list)
+ if (!list) { return }
+ const position = utils.listElementDimensions({ id })
+ list.x = position.x
+ list.y = position.y
+ const { x, y } = list
+ return { id, x, y }
+ })
+ lists = lists.filter(list => Boolean(list))
+ context.commit('move', { lists, spaceId })
+ lists = lists.filter(list => list)
+ // update
+ context.dispatch('updateMultiple', lists)
+ const list = context.getters.byId(currentDraggingdListId)
+ context.dispatch('checkIfItemShouldIncreasePageSize', list, { root: true })
+ context.dispatch('broadcast/update', { updates: { lists }, type: 'moveLists', handler: 'currentLists/moveBroadcast' }, { root: true })
+ context.dispatch('history/resume', null, { root: true })
+ context.dispatch('history/add', { lists, useSnapshot: true }, { root: true })
+ nextTick(() => {
+ context.dispatch('currentConnections/updateMultiplePaths', lists, { root: true })
+ })
+ },
+
+ // remove
+
+ remove: async (context, list) => {
+ context.dispatch('broadcast/update', { updates: list, type: 'removedList', handler: 'currentLists/remove' }, { root: true })
+ context.commit('remove', list)
+ context.dispatch('history/add', { lists: [list], isRemoved: true }, { root: true })
+ await context.dispatch('api/addToQueue', { name: 'removedList', body: list }, { root: true })
+ }
+ },
+ getters: {
+ byId: (state) => (id) => {
+ return state.lists[id]
+ },
+ all: (state) => {
+ return state.ids.map(id => state.lists[id])
+ },
+ isSelectableInViewport: (state, getters) => {
+ const elements = document.querySelectorAll('.list')
+ let lists = []
+ elements.forEach(list => {
+ if (list.dataset.isVisibleInViewport === 'false') { return }
+ if (list.dataset.isLocked === 'true') { return }
+ lists.push(list)
+ })
+ lists = lists.map(list => getters.byId(list.dataset.listId))
+ return lists
+ },
+ isSelectedIds: (state, getters, rootState, rootGetters) => {
+ const currentDraggingId = rootState.currentDraggingdListId
+ const multipleSelectedIds = rootState.multipleListsSelectedIds
+ let listIds = multipleSelectedIds.concat(currentDraggingId)
+ listIds = uniq(listIds)
+ listIds = listIds.filter(id => Boolean(id))
+ return listIds
+ },
+ isResizingIds: (state, getters, rootState) => {
+ let listIds = rootState.currentUserIsResizingdListIds
+ if (getters.isSelectedIds.length) {
+ listIds = getters.isSelectedIds
+ }
+ return listIds
+ },
+ isSelected: (state, getters) => {
+ const listIds = getters.isSelectedIds
+ const lists = listIds.map(id => getters.byId(id))
+ return lists
+ }
+ // isNotLocked: (state, getters) => {
+ // let lists = getters.all
+ // return lists.filter(list => !list.isLocked)
+ // },
+ // isLocked: (state, getters) => {
+ // let lists = getters.all
+ // return lists.filter(list => list.isLocked)
+ // },
+ // colors: (state, getters) => {
+ // const lists = getters.all
+ // let colors = lists.map(list => list.color)
+ // colors = colors.filter(color => Boolean(color))
+ // return uniq(colors)
+ // },
+ // newSnapGuide: (state) => ({ side, item, targetdList }) => {
+ // let time = Date.now()
+ // const prevGuide = state.snapGuides.find(guide => guide.side === side)
+ // if (prevGuide) {
+ // time = prevGuide.time
+ // }
+ // return { side, origin: item, target: targetdList, time }
+ // }
+ }
+}
diff --git a/src/store/store.js b/src/store/store.js
index 016dca59d..4a57d72ec 100644
--- a/src/store/store.js
+++ b/src/store/store.js
@@ -11,6 +11,7 @@ import currentSpace from '@/store/currentSpace.js'
import currentCards from '@/store/currentCards.js'
import currentConnections from '@/store/currentConnections.js'
import currentBoxes from '@/store/currentBoxes.js'
+import currentLists from '@/store/currentLists.js'
import upload from '@/store/upload.js'
import userNotifications from '@/store/userNotifications.js'
import groups from '@/store/groups.js'
@@ -2175,6 +2176,7 @@ const store = createStore({
currentCards,
currentConnections,
currentBoxes,
+ currentLists,
upload,
userNotifications,
groups,
diff --git a/src/utils.js b/src/utils.js
index 67c9965c0..a63e51892 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1163,7 +1163,7 @@ export default {
},
boundaryRectFromItems (items) {
items = this.clone(items)
- items = items.filter(item => item.x && item.y)
+ items = items.filter(item => item?.x && item?.y)
items = items.map(item => {
const defaultSize = 200
item.width = item.resizeWidth || item.width || defaultSize
diff --git a/src/views/Space.vue b/src/views/Space.vue
index 9ed6e20c6..7846ea80a 100644
--- a/src/views/Space.vue
+++ b/src/views/Space.vue
@@ -293,6 +293,13 @@ const addCardFromOutsideAppContext = (event) => {
if (card.spaceId !== currentSpace.id) { return }
store.commit('currentCards/create', { card, shouldPreventCache: true })
}
+const checkIfShouldSnapCards = (event) => {
+ if (!store.state.cardsWereDragged) { return }
+ if (event.shiftKey) { return }
+ const snapGuide = store.state.currentCards.snapGuide
+ if (!snapGuide) { return }
+ store.dispatch('currentCards/snap', { snapGuide })
+}
// boxes
@@ -572,6 +579,7 @@ const stopInteractions = async (event) => {
store.commit('triggerUpdateHeaderAndFooterPosition')
}
checkIfShouldHideFooter(event)
+ checkIfShouldSnapCards(event)
checkIfShouldSnapBoxes(event)
checkIfShouldExpandBoxes(event)
if (shouldCancelInteraction(event)) { return }