├── .gitignore ├── screenshot.png ├── src ├── assets │ ├── Logo.icns │ ├── Logo.ico │ ├── Logo.png │ ├── images │ │ ├── sos.png │ │ ├── avatars │ │ │ ├── adam.png │ │ │ ├── nick.png │ │ │ ├── orion.png │ │ │ ├── pvh.png │ │ │ ├── martin.png │ │ │ ├── natasha.png │ │ │ └── roshan.png │ │ ├── delta.svg │ │ ├── switch-on.svg │ │ ├── switch-off.svg │ │ ├── microscope.svg │ │ ├── documents.svg │ │ ├── comment.svg │ │ ├── trash.svg │ │ └── peers.svg │ └── fonts │ │ ├── Pacifico.ttf │ │ ├── Nunito-Bold.ttf │ │ ├── Nunito-Regular.ttf │ │ └── OpenSans-ExtraBold.ttf ├── lib │ ├── uuid.js │ ├── seed_data.js │ └── store.js ├── components │ ├── assignments │ │ ├── assignments.css │ │ └── assignments.jsx │ ├── list │ │ ├── list.css │ │ └── list.jsx │ ├── flash │ │ ├── flash.css │ │ └── flash.jsx │ ├── automerge_info.jsx │ ├── add_list │ │ ├── add_list.css │ │ └── add_list.jsx │ ├── board │ │ ├── board.css │ │ └── board.jsx │ ├── inspector │ │ ├── inspector.css │ │ └── inspector.jsx │ ├── add_card │ │ ├── add_card.css │ │ └── add_card.jsx │ ├── list_card │ │ ├── list_card.css │ │ └── list_card.jsx │ ├── drop_target.jsx │ ├── inline_input.jsx │ ├── card │ │ ├── card.jsx │ │ └── card.css │ ├── comments.jsx │ ├── documents.jsx │ ├── changes.jsx │ ├── network.jsx │ └── app.jsx ├── index.html ├── index.js └── index.css ├── .eslintrc ├── .compilerc ├── LICENSE ├── package.json ├── test ├── merge-root.trellis ├── merge-fork-a.trellis ├── merge-fork-b.trellis ├── application.js └── fixture.trellis └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | trellis/ 4 | .env 5 | *.swp 6 | *.trellis 7 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/assets/Logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/Logo.icns -------------------------------------------------------------------------------- /src/assets/Logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/Logo.ico -------------------------------------------------------------------------------- /src/assets/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/Logo.png -------------------------------------------------------------------------------- /src/assets/images/sos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/sos.png -------------------------------------------------------------------------------- /src/assets/fonts/Pacifico.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/fonts/Pacifico.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Nunito-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/fonts/Nunito-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Nunito-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/fonts/Nunito-Regular.ttf -------------------------------------------------------------------------------- /src/assets/images/avatars/adam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/adam.png -------------------------------------------------------------------------------- /src/assets/images/avatars/nick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/nick.png -------------------------------------------------------------------------------- /src/assets/images/avatars/orion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/orion.png -------------------------------------------------------------------------------- /src/assets/images/avatars/pvh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/pvh.png -------------------------------------------------------------------------------- /src/assets/images/avatars/martin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/martin.png -------------------------------------------------------------------------------- /src/assets/images/avatars/natasha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/natasha.png -------------------------------------------------------------------------------- /src/assets/images/avatars/roshan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/images/avatars/roshan.png -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/trellis/HEAD/src/assets/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/lib/uuid.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 3 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 4 | return v.toString(16); 5 | }) 6 | 7 | return uuid 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "rules": { 4 | "import/extensions": 0, 5 | "import/no-extraneous-dependencies": 0, 6 | "import/no-unresolved": [2, { "ignore": ["electron"] }], 7 | "linebreak-style": 0, 8 | "react/prefer-stateless-function": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/assignments/assignments.css: -------------------------------------------------------------------------------- 1 | .Assignments { 2 | margin-top: 10pt; 3 | } 4 | 5 | .Assignments img { 6 | width: 15pt; 7 | margin-left: 4pt; 8 | } 9 | 10 | .Assignments img:first-child { 11 | margin-left: 0; 12 | } 13 | 14 | .Assignments__active { 15 | opacity: 0.8; 16 | } 17 | .Assignments__inactive { 18 | opacity: 0.2; 19 | filter: grayscale(100%) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/components/list/list.css: -------------------------------------------------------------------------------- 1 | .List { 2 | margin-right: 20px; 3 | padding: 10px; 4 | width: 240px; 5 | overflow-y: scroll; 6 | flex-shrink: 0; 7 | color: var(--color-light); 8 | } 9 | 10 | .List__title { 11 | text-transform: uppercase; 12 | font-weight: bold; 13 | margin-bottom: 10px; 14 | font-family: "Open Sans"; 15 | font-size: 18px; 16 | } 17 | 18 | .List__delete { 19 | float: right; 20 | cursor: pointer; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/seed_data.js: -------------------------------------------------------------------------------- 1 | import uuid from './uuid' 2 | 3 | export default function() { 4 | let lists = [ { id: uuid(), title: "This Week" }, 5 | { id: uuid(), title: "Done" }, 6 | { id: uuid(), title: "Soon" } ] 7 | 8 | let cards = [ { id: uuid(), 9 | listId: lists[0].id, 10 | title: "Hello world", 11 | assigned: {} } ] 12 | 13 | return { lists: lists, cards: cards } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/flash/flash.css: -------------------------------------------------------------------------------- 1 | @keyframes slide-down { 2 | 0% { top: -200px } 3 | 5% { top: 20px } 4 | 90% { top: 20px } 5 | 100% { top: -200px } 6 | } 7 | 8 | .Flash { 9 | background: var(--color-change-highlight-bg); 10 | color: var(--color-dark); 11 | position: absolute; 12 | top: -200px; 13 | left: 50%; 14 | margin-left: -40%; 15 | width: 80%; 16 | padding: 20px; 17 | box-sizing: border-box; 18 | text-align: center; 19 | animation: slide-down 2s linear 0s 1 normal forwards; 20 | border-radius: 2px; 21 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.1) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 24 | 25 | -------------------------------------------------------------------------------- /.compilerc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "application/javascript": { 5 | "presets": [ 6 | ["env", { "targets": { "electron": "1.6.0" } }], 7 | "react" 8 | ], 9 | "plugins": ["transform-async-to-generator", "transform-es2015-classes"], 10 | "sourceMaps": "inline" 11 | } 12 | }, 13 | "production": { 14 | "application/javascript": { 15 | "presets": [ 16 | ["env", { "targets": { "electron": "1.6.0" } }], 17 | "react" 18 | ], 19 | "plugins": ["transform-async-to-generator", "transform-es2015-classes"], 20 | "sourceMaps": "none" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/automerge_info.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class AutomergeInfo extends React.Component { 4 | constructor(props) { 5 | super(props) 6 | } 7 | 8 | render() { 9 | let t = this.props.Automerge 10 | let peers = Object.keys(t.peers).map((peer) => { return peer }) 11 | let peer_actions = Object.keys(t.peer_actions).map((pa) => { 12 | return pa + "(" + t.peer_actions[pa].length + ") " 13 | }) 14 | 15 | return ( 16 |
17 | 22 |
23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/flash/flash.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Flash extends React.Component { 4 | constructor() { 5 | super() 6 | this.state = {} 7 | } 8 | 9 | show(message) { 10 | // First clear out previous flash if it exists 11 | this.setState({ message: undefined }) 12 | this.setState({ message: message }) 13 | 14 | // The timing here must match up with whatever 15 | // CSS animations are applied to .Flash 16 | setTimeout(() => { 17 | this.setState({ message: undefined }) 18 | }, 6000) 19 | } 20 | 21 | render() { 22 | if(this.state.message) 23 | return
{ this.state.message }
24 | else 25 | return null 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/images/delta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | delta 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/add_list/add_list.css: -------------------------------------------------------------------------------- 1 | .AddList { 2 | margin-right: 20px; 3 | padding: 10px; 4 | border-radius: 4px; 5 | width: 200px; 6 | flex-shrink: 0; 7 | } 8 | 9 | .AddList__show:hover { 10 | cursor: pointer; 11 | text-decoration: underline; 12 | } 13 | 14 | .AddList input[type="text"] { 15 | border: none; 16 | padding: 10px; 17 | font-size: 14px; 18 | border-radius: 5px; 19 | box-sizing: border-box; 20 | width: 100%; 21 | margin-bottom: 10px; 22 | } 23 | 24 | .AddList input[type="text"]:focus { 25 | outline: none; 26 | } 27 | 28 | .AddList__save { 29 | padding: 5px 15px; 30 | font-size: 16px; 31 | border-radius: 5px; 32 | background-color: #2980b9; 33 | color: white; 34 | cursor: pointer; 35 | } 36 | 37 | .AddList__clearForm { 38 | float: right; 39 | cursor: pointer; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/board/board.css: -------------------------------------------------------------------------------- 1 | .Board { 2 | grid-row: 1 / 4; 3 | grid-column: 1 / 4; 4 | padding-left: 15pt; 5 | display: flex; 6 | flex-direction: column; 7 | background-color: var(--color-app); 8 | font-family: "Nunito"; 9 | position: relative; 10 | } 11 | 12 | .Board textarea, .Board input[type="text"] { 13 | font-family: "Nunito"; 14 | } 15 | 16 | .Board__lists { 17 | display: flex; 18 | justify-content: flex-start; 19 | overflow: auto; 20 | flex-grow: 1; 21 | margin-bottom: 20px; 22 | } 23 | 24 | .Board__title .InlineInput div, .Board__title .InlineInput textarea { 25 | text-align: center; 26 | font-family: "Open Sans"; 27 | margin: 0; 28 | flex-grow: 0; 29 | flex-shrink: 0; 30 | color: var(--color-light); 31 | padding: 20px 0 40px 0; 32 | font-size: 28px; 33 | width: 100%; 34 | height: 28px; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/inspector/inspector.css: -------------------------------------------------------------------------------- 1 | .Inspector table { 2 | border-collapse: collapse; 3 | margin-right: 40px; 4 | } 5 | 6 | .Inspector table th { 7 | background: #CCC; 8 | } 9 | 10 | .Inspector table th, .Inspector table td { 11 | padding: 5px 10px; 12 | border: 1px solid #AAA; 13 | text-align: left; 14 | } 15 | 16 | .Inspector table tr.highlight > td { 17 | background: #FFBA08; 18 | transition: background 0.5s; 19 | } 20 | .Inspector table tr > td { 21 | transition: background 1.5s; 22 | } 23 | 24 | .Inspector__cards__cardId, .Inspector__cards__listId, 25 | .Inspector__cards__assigned, .Inspector__lists__listId { 26 | font-family: Monaco, monospace; 27 | } 28 | 29 | .Inspector input[type="text"] { 30 | border: 1px solid #aaa; 31 | } 32 | .Inspector input[type="text"].number { 33 | width: 12pt; 34 | } 35 | .Inspector input[type="text"].string { 36 | width: 140pt; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/add_card/add_card.css: -------------------------------------------------------------------------------- 1 | .AddCard__link { 2 | cursor: pointer; 3 | padding-top: 10px; 4 | } 5 | 6 | .AddCard__link:hover { 7 | text-decoration: underline; 8 | } 9 | 10 | .AddCard textarea { 11 | box-sizing: border-box; 12 | width: 100%; 13 | padding: 10px; 14 | border: none; 15 | border-radius: 4px; 16 | font-size: 14px; 17 | color: #666; 18 | box-shadow: 0 2px 10px 0px #59ADD0; 19 | min-height: 72px; 20 | } 21 | 22 | .AddCard textarea:focus { 23 | outline: none; 24 | } 25 | 26 | .AddCard button { 27 | border: none; 28 | font-size: 16px; 29 | padding: 5px 15px; 30 | margin: 5px 0; 31 | border-radius: 5px; 32 | background-color: rgba(29,119,156, 0.7); 33 | color: white; 34 | cursor: pointer; 35 | } 36 | 37 | .AddCard a { 38 | margin-top: 8px; 39 | margin-right: 10px; 40 | color: rgba(29,119,156, 0.7); 41 | text-decoration: none; 42 | float: right; 43 | } 44 | 45 | .AddCard a:hover { 46 | font-weight: bold; 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ink & Switch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/components/list_card/list_card.css: -------------------------------------------------------------------------------- 1 | .ListCard { 2 | background: white; 3 | padding: 10px; 4 | border-radius: 4px; 5 | margin-bottom: 10px; 6 | font-size: 14px; 7 | border: 1px solid #ccc; 8 | box-shadow: 0 2px 10px 0px rgba(0,0,0,0.2); 9 | color: #777; 10 | } 11 | 12 | .ListCard__title { 13 | float: left; 14 | width: 80%; 15 | margin-bottom: 20px; 16 | } 17 | 18 | .ListCard__title:hover { 19 | cursor: pointer; 20 | } 21 | 22 | .ListCard__delete:hover { 23 | color: #333; 24 | } 25 | 26 | .ListCard textarea { 27 | font-size: 14px; 28 | border: none; 29 | outline: none; 30 | margin-top: -1px; 31 | margin-left: -2px; 32 | } 33 | 34 | .ListCard.highlighted { 35 | box-shadow: 0 0 1px 4px #FFBA08; 36 | color: #FFBA08; 37 | border: 1px solid #FFBA08; 38 | } 39 | .ListCard { 40 | transition: box-shadow 1s, color 1s, border 1s; 41 | } 42 | 43 | .ListCard__commentsCount { 44 | float: left; 45 | margin-top: 2px; 46 | } 47 | 48 | .ListCard__commentsCount__count { 49 | display: inline-block; 50 | margin-left: 3px; 51 | font-size: 11px; 52 | vertical-align: top; 53 | } 54 | 55 | .ListCard .Assignments { 56 | float: right; 57 | margin-top: 0; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/assignments/assignments.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Assignments extends React.Component { 4 | constructor() { 5 | super() 6 | this.toggle = this.toggle.bind(this) 7 | } 8 | 9 | card() { 10 | return this.props.store.findCard(this.props.cardId) 11 | } 12 | 13 | assigned() { 14 | let a = this.card().assigned 15 | return a ? a : {} 16 | } 17 | 18 | toggle(event) { 19 | if(this.props.readonly) 20 | return 21 | 22 | event.stopPropagation() 23 | 24 | let person = event.target.name 25 | let isAssigned = !this.assigned()[person] 26 | 27 | this.props.store.dispatch({ 28 | type: "UPDATE_ASSIGNMENTS", 29 | cardId: this.props.cardId, 30 | person: person, 31 | isAssigned: isAssigned 32 | }) 33 | } 34 | 35 | // Hardcoded names used for demo and prototyping 36 | people() { 37 | if (process.env.AVENGERS) 38 | return [ 'nick', 'natasha' ] 39 | else 40 | return [ 'adam', 'orion', 'pvh', 'roshan', 'martin' ] 41 | } 42 | 43 | render() { 44 | let people = this.people() 45 | if (this.props.readonly) 46 | people = people.filter((person) => this.assigned()[person]) 47 | 48 | let assignments = people.map((person) => { 49 | let fname = "assets/images/avatars/" + person + ".png" 50 | let klass = this.assigned()[person] ? "Assignments__active" : "Assignments__inactive" 51 | return 52 | }) 53 | return ( 54 |
55 | { assignments} 56 |
57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/assets/images/switch-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | switch-on 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/add_list/add_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class AddList extends React.Component { 4 | constructor() { 5 | super() 6 | 7 | this.showForm = this.showForm.bind(this) 8 | this.clearForm = this.clearForm.bind(this) 9 | this.createList = this.createList.bind(this) 10 | this.updateTitle = this.updateTitle.bind(this) 11 | 12 | this.state = { 13 | title: "", 14 | showForm: false 15 | } 16 | } 17 | 18 | showForm() { 19 | this.setState({ showForm: true }, () => this.titleInput.focus()) 20 | } 21 | 22 | clearForm() { 23 | this.setState({ title: "", showForm: false }) 24 | } 25 | 26 | updateTitle(event) { 27 | this.setState({ title: event.target.value }) 28 | } 29 | 30 | createList() { 31 | this.props.store.dispatch({ 32 | type: "CREATE_LIST", 33 | attributes: { 34 | title: this.state.title 35 | } 36 | }) 37 | 38 | this.clearForm() 39 | } 40 | 41 | render() { 42 | let partial 43 | 44 | if(this.state.showForm) { 45 | partial = 46 |
47 | this.titleInput = input } 49 | type="text" placeholder="Add a list…" onChange={ this.updateTitle } /> 50 |
51 | Save 52 | X 53 |
54 |
55 | } else { 56 | partial = Add a list… 57 | } 58 | 59 | return ( 60 |
61 | { partial } 62 |
63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/list/list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ListCard from '../list_card/list_card' 3 | import AddCard from '../add_card/add_card' 4 | import DropTarget from '../drop_target' 5 | 6 | export default class List extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.delete = this.delete.bind(this) 10 | } 11 | 12 | list() { 13 | return this.props.store.findList(this.props.listId) 14 | } 15 | 16 | delete() { 17 | this.props.store.dispatch({ 18 | type: "DELETE_LIST", 19 | listId: this.props.listId 20 | }) 21 | } 22 | 23 | render() { 24 | let listCards = this.props.store.findCardsByList(this.props.listId) 25 | let listCardsPartial = listCards.map((card) => { 26 | let commentsCount = this.props.store.findCommentsByCard(card.id).length 27 | 28 | return 29 | 36 | 37 | }) 38 | 39 | return ( 40 |
41 | 42 | 43 |
{ this.list().title }
44 |
45 | { listCardsPartial } 46 | 47 |
48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/drop_target.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class DropTarget extends React.Component { 5 | constructor() { 6 | super() 7 | this.onDrop = this.onDrop.bind(this) 8 | this.onDragEnter = this.onDragEnter.bind(this) 9 | this.onDragLeave = this.onDragLeave.bind(this) 10 | this.counter = 0 11 | } 12 | 13 | preventDefault(event) { 14 | event.preventDefault() 15 | } 16 | 17 | onDrop(event) { 18 | event.currentTarget.classList.remove("drag-entered") 19 | this.counter = 0 20 | 21 | let cardId = event.dataTransfer.getData("text") 22 | 23 | this.props.store.dispatch({ 24 | type: "MOVE_CARD", 25 | cardId: cardId, 26 | listId: this.props.listId, 27 | afterCardId: this.props.afterCardId 28 | }) 29 | } 30 | 31 | // Every child of our DropTarget also triggers 'onDragEnter' 32 | // and 'onDragLeave' events, so we need to keep a counter to 33 | // track when we've entered or left the top-level element 34 | onDragEnter(event) { 35 | if(this.counter === 0) 36 | event.currentTarget.classList.add("drag-entered") 37 | 38 | this.counter += 1 39 | } 40 | 41 | onDragLeave(event) { 42 | this.counter -= 1 43 | 44 | if(this.counter === 0) 45 | event.currentTarget.classList.remove("drag-entered") 46 | } 47 | 48 | render() { 49 | // Chrome has a drag-and-drop bug that requires onDragOver to not propogate its event 50 | return
56 | { this.props.children } 57 |
58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/inline_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class InlineInput extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | 8 | this.handleKeyDown = this.handleKeyDown.bind(this) 9 | this.handleBlur = this.handleBlur.bind(this) 10 | this.edit = this.edit.bind(this) 11 | this.state = { editMode: false } 12 | } 13 | 14 | edit() { 15 | this.setState({ editMode: true }, () => this.input.focus()) 16 | } 17 | 18 | handleKeyDown(event) { 19 | let doSubmit = !event.shiftKey && event.key === "Enter" 20 | if (doSubmit && this.props.onSubmit) 21 | this.props.onSubmit(event.target.value) 22 | 23 | // Exit edit mode if "Enter" or "Esc" are pressed 24 | if (doSubmit || event.keyCode === 27) { 25 | event.stopPropagation() 26 | this.setState({ editMode: false }) 27 | } 28 | } 29 | 30 | handleBlur() { 31 | this.setState({ editMode: false }) 32 | } 33 | 34 | render() { 35 | let children, label 36 | 37 | if(this.state.editMode) { 38 | children =