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 }