Skip to content

Instantly share code, notes, and snippets.

@Stradivario
Last active October 17, 2020 16:36
Show Gist options
  • Save Stradivario/4a68a8894c27883c89c88df3ea735c46 to your computer and use it in GitHub Desktop.
Save Stradivario/4a68a8894c27883c89c88df3ea735c46 to your computer and use it in GitHub Desktop.
Example Trello board web component
/* 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