Skip to content

Commit 64c1421

Browse files
committed
docs(examples/kanban): PoC for userspace copying
1 parent 3b2bb97 commit 64c1421

File tree

3 files changed

+116
-36
lines changed

3 files changed

+116
-36
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface Card {
22
id: number;
33
title: string;
4+
isCopy?: boolean;
45
}
56

67
export type Cards = ReadonlyArray<Card>;

packages/examples/src/app/sortable/kanban/kanban-board/kanban-board.component.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DraggedItem, NgRxSortable } from "angular-skyhook-card-list";
33
import { KanbanList } from '../lists';
44
import { Card } from "../card";
55
import { ItemTypes } from "../item-types";
6-
import { ActionTypes, AddCard, RemoveCard, _render, _listById } from "../store";
6+
import { ActionTypes, AddCard, RemoveCard, _render, _listById, _isCopying, CARD_ID_WHEN_COPYING } from "../store";
77
import { Store, select } from '@ngrx/store';
88

99
@Component({
@@ -25,10 +25,51 @@ export class KanbanBoardComponent {
2525
getList: _listId => this.store.pipe(select(_render)),
2626
});
2727

28+
isCopying: boolean;
29+
subs = this.store.pipe(select(_isCopying))
30+
.subscribe(x => this.isCopying = x);
31+
2832
listSpec = new NgRxSortable<Card>(this.store, ActionTypes.SortCard, {
2933
trackBy: card => card.id,
3034
// here we use the different listId on each kanban-list to pull different data
3135
getList: listId => this.store.pipe(select(_listById(listId))),
36+
37+
// isDragging determines which card on the ground will regard itself as
38+
// "the same as the one in flight". It must return true for exactly one
39+
// card at a time, and that card MUST be placed under the most recently
40+
// hovered DraggedItem.
41+
//
42+
// By default, it is defined as
43+
//
44+
// trackBy(ground) === trackBy(inFlight.data).
45+
//
46+
// But we want to be able to copy cards around -- so when there's an
47+
// extra clone in transit around the board, we have to be careful to
48+
// implement isDragging correctly.
49+
50+
// In this case:
51+
//
52+
// 1. We set id = a unique CARD_ID_WHEN_COPYING on any clones (if they
53+
// kept the same ID, there would be ngFor anomalies due to trackBy).
54+
// See store.ts.
55+
//
56+
// 2. We don't get to modify the inFlight data, so instead, we compare
57+
// ground.id to CARD_ID_WHEN_COPYING when we're copying.
58+
//
59+
// You can see for yourself that there is never more than one card with
60+
// CARD_ID_WHEN_COPYING, so:
61+
//
62+
// a. trackBy still returns a different value for every card on the
63+
// board;
64+
// b. exactly one card will return true from isDragging.
65+
// c. that card will be the clone if copying, otherwise the original.
66+
// d. as long as the clone follows the hover around like the original
67+
// normally does, it stays in place.
68+
69+
isDragging: (ground, inFlight) => {
70+
let flyingId = this.isCopying ? CARD_ID_WHEN_COPYING : inFlight.data.id;
71+
return ground.id === flyingId;
72+
}
3273
});
3374

3475
constructor(public store: Store<{}>) { }
@@ -41,4 +82,8 @@ export class KanbanBoardComponent {
4182
this.store.dispatch(new RemoveCard(ev));
4283
}
4384

85+
ngOnDestroy() {
86+
this.subs.unsubscribe();
87+
}
88+
4489
}

packages/examples/src/app/sortable/kanban/store.ts

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ export interface BoardState {
4343
/** Holds a modified version of `board` that DOESN'T contain whatever item is in-flight,
4444
* or null if no item has currently been picked up from a sortable. */
4545
draggingBoard: KanbanBoard | null;
46-
4746
// Hold in-flight items in state so we can inject them back into draggingBoard, in a selector
4847
cardInFlight: DraggedItem<Card> | null;
4948
listInFlight: DraggedItem<KanbanList> | null;
50-
5149
nextId: number;
5250
spilledCard: boolean;
51+
isCopying: boolean;
52+
shouldCopy: boolean;
5353
}
5454

5555
export const initialState = {
@@ -59,8 +59,47 @@ export const initialState = {
5959
listInFlight: null,
6060
nextId: 1000,
6161
spilledCard: false,
62+
isCopying: false,
63+
shouldCopy: false,
6264
};
6365

66+
const resetDrag = (state: BoardState): BoardState => ({
67+
...state,
68+
draggingBoard: null,
69+
cardInFlight: null,
70+
listInFlight: null,
71+
isCopying: false,
72+
spilledCard: false
73+
});
74+
75+
export const CARD_ID_WHEN_COPYING = Symbol("CLONED_CARD") as any;
76+
const cloneCard = (card: Card, nextId: any) => ({ ...card, id: nextId });
77+
78+
// - When you're not copying, draggingBoard holds 'board - cardInFlight.data',
79+
// so you can just insertCard wherever it is meant to go.
80+
// - When you ARE copying, you don't care: you want 'board + clone of
81+
// cardInFlight.data'
82+
// - So this function returns whichever one depending on isCopying, and neatly
83+
// increments nextId for you (so it gives you a whole state back).
84+
const copyOrInsertCard = (state: BoardState, clonedCard: Card): BoardState => {
85+
let { board, nextId } = state;
86+
if (state.cardInFlight) {
87+
const { data, hover } = state.cardInFlight;
88+
if (state.isCopying) {
89+
nextId++;
90+
board = state.spilledCard
91+
? state.board
92+
: insertCard(state.board, clonedCard, hover.listId, hover.index);
93+
} else {
94+
const either = state.draggingBoard || state.board;
95+
board = state.spilledCard
96+
? either
97+
: insertCard(either, data, hover.listId, hover.index);
98+
}
99+
}
100+
return { ...resetDrag(state), board, nextId };
101+
}
102+
64103
// Each of these functions is a 'mini-reducer' dedicated to handling sort events.
65104
// `action.event` is like `action.type`, so use it the same way with a switch statement.
66105

@@ -80,21 +119,18 @@ export function listReducer(state: BoardState, action: SortList) {
80119
}
81120
case SortableEvents.Drop: {
82121
return {
83-
...state,
122+
...resetDrag(state),
84123
board: insertList(currentBoard, data, hover.index),
85-
draggingBoard: null,
86-
listInFlight: null
87124
};
88125
}
89126
case SortableEvents.EndDrag: {
90-
return { ...state, draggingBoard: null, listInFlight: null };
127+
return resetDrag(state);
91128
}
92129
default: return state;
93130
}
94131
}
95132

96133
export function cardReducer(state: BoardState, action: SortCard) {
97-
const currentBoard = state.draggingBoard || state.board;
98134
const { data, index, listId, hover } = action.item;
99135

100136
// turn off 'spill' any time a reordering happens, because that means card has left spill area
@@ -104,22 +140,18 @@ export function cardReducer(state: BoardState, action: SortCard) {
104140
return {
105141
...state,
106142
draggingBoard: removeCard(state.board, listId, index),
107-
cardInFlight: action.item
143+
cardInFlight: action.item,
144+
isCopying: state.shouldCopy && listId === 1,
108145
};
109146
}
110147
case SortableEvents.Hover: {
111148
return { ...state, cardInFlight: action.item };
112149
}
113150
case SortableEvents.Drop: {
114-
return {
115-
...state,
116-
board: insertCard(currentBoard, data, hover.listId, hover.index),
117-
draggingBoard: null,
118-
cardInFlight: null
119-
};
151+
return copyOrInsertCard(state, cloneCard(state.cardInFlight.data, state.nextId));
120152
}
121153
case SortableEvents.EndDrag: {
122-
return { ...state, draggingBoard: null, cardInFlight: null };
154+
return resetDrag(state);
123155
}
124156
default: return state;
125157
}
@@ -160,11 +192,7 @@ export function reducer(state: BoardState = initialState, action: Actions): Boar
160192
}
161193

162194
case ActionTypes.Spill: {
163-
return {
164-
...state,
165-
spilledCard: true,
166-
cardInFlight: action.item
167-
}
195+
return { ...state, spilledCard: true, cardInFlight: action.item }
168196
}
169197

170198
default:
@@ -173,11 +201,17 @@ export function reducer(state: BoardState = initialState, action: Actions): Boar
173201
}
174202

175203
const _boardState = createFeatureSelector<BoardState>('kanban');
176-
const _board = createSelector(_boardState, state => state.draggingBoard || state.board);
177-
const _cardInFlight = createSelector(_boardState, state => state.cardInFlight);
178-
const _listInFlight = createSelector(_boardState, state => state.listInFlight);
179-
const _spilledCard = createSelector(_boardState, state => state.spilledCard);
180204

205+
export const _isCopying = createSelector(_boardState, s => s.isCopying);
206+
export const _cardInFlight = createSelector(_boardState, state => state.cardInFlight);
207+
// produce a clone to be inserted into the board while dragging
208+
// a permanent one is created on drop
209+
export const _temporaryClone = createSelector(
210+
_isCopying,
211+
_cardInFlight,
212+
(copying, card) => (copying && card) ? cloneCard(card.data, CARD_ID_WHEN_COPYING) : null);
213+
214+
const _board = createSelector(_boardState, state => state.board);
181215
export const _listById = (listId: any) => createSelector(_board, board => {
182216
const list = board.find(l => l.id === listId);
183217
return list && list.cards;
@@ -194,19 +228,19 @@ export const _listById = (listId: any) => createSelector(_board, board => {
194228
// called. Then insertXXX is called here -- and it works.
195229

196230
export const _render = createSelector(
197-
_board,
198-
_cardInFlight,
199-
_listInFlight,
200-
_spilledCard,
201-
(board, cardInFlight, listInFlight, spilledCard) => {
202-
if (cardInFlight != null && !spilledCard) {
203-
const { index, listId } = cardInFlight.hover;
204-
board = insertCard(board, cardInFlight.data, listId, index);
231+
_boardState,
232+
_temporaryClone,
233+
(state, tempClone) => {
234+
const { cardInFlight, listInFlight } = state;
235+
let either = state.draggingBoard || state.board;
236+
237+
if (cardInFlight != null) {
238+
return copyOrInsertCard(state, tempClone).board;
205239
}
206240
if (listInFlight != null) {
207-
const { index, listId } = listInFlight.hover;
208-
board = insertList(board, listInFlight.data, index);
241+
const { hover, data } = listInFlight;
242+
return insertList(either, data, hover.index);
209243
}
210-
return board;
244+
return either;
211245
}
212246
);

0 commit comments

Comments
 (0)