Last active
October 17, 2020 16:36
-
-
Save Stradivario/4a68a8894c27883c89c88df3ea735c46 to your computer and use it in GitHub Desktop.
Example Trello board web component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-this-alias */ | |
import { Component, css, html, LitElement, property } from '@rxdi/lit-html'; | |
export interface Board { | |
id: string; | |
name: string; | |
cards: Card[]; | |
} | |
export interface Card { | |
id: string; | |
title: string; | |
} | |
/** | |
* @customElement trello-board | |
*/ | |
@Component({ | |
selector: 'trello-board', | |
style: css` | |
.container { | |
font-family: 'Source Sans Pro', 'Roboto', 'Helvetica Neue', Helvetica, Arial, | |
sans-serif; | |
padding: 16px; | |
color: #3c3c3c; | |
font-size: 14px; | |
margin: 0; | |
} | |
.panel-header .panel-title { | |
font-size: 18px; | |
color: #3171b2; | |
font-weight: 700; | |
font-family: inherit; | |
padding-top: 1px; | |
} | |
.panel-header .panel-desc { | |
font-size: 13px; | |
color: #1c2e36; | |
display: block; | |
} | |
.panel-header { | |
padding: 0 0 6px; | |
border-bottom: 1px solid #d8d8d8; | |
position: relative; | |
} | |
.panel-body { | |
height: calc(100vh - 105px); | |
margin: 0 -5px; | |
padding: 15px 0; | |
} | |
.list { | |
margin: 0 5px; | |
display: inline-block; | |
width: 200px; | |
/* padding: 8px; */ | |
vertical-align: top; | |
position: relative; | |
z-index: 1; | |
height: 100%; | |
} | |
.list > .title { | |
font-weight: bold; | |
display: block; | |
/* padding-bottom: 6px; */ | |
position: absolute; | |
top: 8px; | |
left: 8px; | |
} | |
.list > .task-list { | |
list-style: none; | |
padding: 34px 8px 34px 8px; | |
margin: 0; | |
background-color: #e2e4e6; | |
border-radius: 3px; | |
} | |
.list > .task-list li { | |
background-color: white; | |
padding: 5px; | |
border-radius: 3px; | |
/* margin-top: 6px; */ | |
margin-bottom: 6px; | |
width: 184px; | |
z-index: 2; | |
box-sizing: border-box; | |
} | |
.list > .task-list li#card-helper { | |
position: fixed; | |
pointer-events: none; | |
} | |
.list > .task-list li.clone { | |
background-color: red; | |
z-index: 3 !important; | |
} | |
.list > .task-list li.card-ghost { | |
background-color: #c4c9cc; | |
pointer-events: none; | |
} | |
.list > .task-list li.dragging-card { | |
position: absolute; | |
opacity: 0; | |
/*pointer-events: none;*/ | |
} | |
.list > .task-list li:hover { | |
cursor: pointer; | |
background-color: #edeff0; | |
} | |
.list .add-card-btn { | |
display: block; | |
padding: 8px 10px; | |
color: #838c91; | |
position: relative; | |
top: -32px; | |
border-radius: 0 0 3px 3px; | |
/*margin-top: -6px;*/ | |
} | |
.list .add-card-btn:hover { | |
background-color: #cdd2d4; | |
color: #4d4d4d; | |
text-decoration: underline; | |
cursor: pointer; | |
} | |
/* Prevent the text contents of draggable elements from being selectable. */ | |
[draggable] { | |
-moz-user-select: none; | |
-khtml-user-select: none; | |
-webkit-user-select: none; | |
user-select: none; | |
/* Required to make elements draggable in old WebKit */ | |
-khtml-user-drag: element; | |
-webkit-user-drag: element; | |
} | |
`, | |
template(this: TrelloCardComponent) { | |
return html` | |
<div class="container"> | |
<div class="roadmap-panel"> | |
<div class="panel-body"> | |
${this.boards?.map( | |
(board) => html` | |
<div class="list"> | |
<span class="title">${board?.name}</span> | |
<ul class="task-list"> | |
${board?.cards.map( | |
(card) => html` | |
<li id=${card?.id} draggable="true"> | |
${card?.title} | |
</li> | |
`, | |
)} | |
</ul> | |
<a | |
@click=${() => | |
this.dispatchEvent( | |
new CustomEvent('onAddCard', { | |
detail: { boardId: board?.id }, | |
}), | |
)} | |
class="add-card-btn" | |
>Add a card...</a | |
> | |
</div> | |
`, | |
)} | |
</div> | |
</div> | |
</div> | |
`; | |
}, | |
}) | |
export class TrelloCardComponent extends LitElement { | |
private draggingCard: HTMLElement; | |
private targetBoard: HTMLElement; | |
private helper: HTMLElement; | |
private helperTrans: { x: number; y: number }; | |
private pos: number; | |
@property({ type: Array }) | |
boards: Board[] = []; | |
OnUpdateFirst() { | |
const boards = Array.from(this.shadowRoot.querySelectorAll('.list')); | |
const cards = Array.from(this.shadowRoot.querySelectorAll('.task-list li')); | |
for (const card of cards) { | |
this.configCardElement(card); | |
} | |
const self = this; | |
for (const board of boards) { | |
board.addEventListener('dragenter', function (this: HTMLDivElement) { | |
self.targetBoard = this.querySelector('ul.task-list'); | |
}); | |
} | |
} | |
private getBoardPosition(board: HTMLElement, yPosition: number) { | |
const boardCards = Array.from(board.querySelectorAll('li:not(card-helper)')); | |
let i = 0; | |
for (const card of boardCards) { | |
const rect = card.getBoundingClientRect(); | |
if (yPosition > rect.top + rect.height) { | |
i++; | |
} | |
} | |
return i; | |
} | |
private configCardElement(card: HTMLElement | Node) { | |
const self = this; | |
card.addEventListener( | |
'dragstart', | |
function (e: MouseEvent) { | |
self.draggingCard = this; | |
self.targetBoard = this.parentElement; | |
self.helper = self.draggingCard.cloneNode(true) as never; | |
const className = self.draggingCard.className.split(' '); | |
className.push('dragging-card'); | |
self.draggingCard.className = className.join(' '); | |
self.helper.id = 'card-helper'; | |
const boards = self.shadowRoot.querySelectorAll('ul.task-list'); | |
boards[boards.length - 1].appendChild(self.helper); | |
self.helperTrans = { | |
x: e.clientX - self.draggingCard.getBoundingClientRect().left, | |
y: e.clientY - self.draggingCard.getBoundingClientRect().top, | |
}; | |
self.helper.style.left = e.clientX - self.helperTrans.x + 'px'; | |
self.helper.style.top = e.clientY - self.helperTrans.y + 'px'; | |
}, | |
false, | |
); | |
card.addEventListener('drag', (e) => this.onDrag(e as never)); | |
card.addEventListener('dragend', (e) => this.onDragEnd(e as never)); | |
} | |
private onDrag(e: MouseEvent) { | |
let placeholder = this.shadowRoot.getElementById('card-placeholder'); | |
if (placeholder) { | |
placeholder.parentNode.removeChild(placeholder); | |
} | |
placeholder = document.createElement('li'); | |
placeholder.id = 'card-placeholder'; | |
placeholder.className = 'card-ghost'; | |
placeholder.style.height = | |
this.draggingCard.getBoundingClientRect().height + 'px'; | |
this.pos = this.getBoardPosition(this.targetBoard, e.clientY); | |
if (this.pos < this.targetBoard.children.length) { | |
this.targetBoard.insertBefore( | |
placeholder, | |
this.targetBoard.children[this.pos], | |
); | |
} else { | |
this.targetBoard.appendChild(placeholder); | |
} | |
this.helper.style.left = e.clientX - this.helperTrans.x + 'px'; | |
this.helper.style.top = e.clientY - this.helperTrans.y + 'px'; | |
} | |
private onDragEnd(e: MouseEvent) { | |
const placeholder = this.shadowRoot.getElementById('card-placeholder'); | |
if (placeholder) { | |
placeholder.parentNode.removeChild(placeholder); | |
} | |
this.draggingCard.className = this.draggingCard.className | |
.replace('dragging-card', '') | |
.trim(); | |
const clone = this.draggingCard.cloneNode(true); | |
this.helper.parentElement.removeChild(this.helper); | |
this.draggingCard.parentElement.removeChild(this.draggingCard); | |
const pos = this.getBoardPosition(this.targetBoard, e.clientY); | |
if (pos < this.targetBoard.children.length) { | |
this.targetBoard.insertBefore(clone, this.targetBoard.children[pos]); | |
} else { | |
this.targetBoard.appendChild(clone); | |
} | |
this.configCardElement(clone); | |
if (clone) { | |
this.dispatchEvent( | |
new CustomEvent('onCardDropped', { | |
detail: { cardId: clone['id'] }, | |
}), | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment