Skip to content

Commit

Permalink
TagInput basic end, and prevent vite auto polling for restart.
Browse files Browse the repository at this point in the history
  • Loading branch information
fecapark committed Jun 4, 2022
1 parent c5b47df commit 1c682c6
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 23 deletions.
21 changes: 18 additions & 3 deletions src/components/Cards/InfoCard/InfoCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@ import "./InfoCard.scss";
import Component from "../../../core/Component/Component";
import { Nullable } from "Animator-Type";

interface CardTitleOptions {
title?: string;
subTitle?: string;
}

export default class InfoCard extends Component {
constructor(public cardContent: Component | Nullable) {
private readonly title: string;
private readonly subTitle: string;

constructor(
public cardContent: Component | Nullable,
{ title = "", subTitle = "" }: CardTitleOptions = {}
) {
super({ classNames: ["info-card"] });

this.title = title;
this.subTitle = subTitle;

this.render();
}

render() {
this.container.innerHTML = `
<div class="card-title-container">
<span class="title">이름은 무엇인가요?</span>
<span class="sub-title">20자 이내로 알려주세요!</span>
<span class="title">${this.title}</span>
<span class="sub-title">${this.subTitle}</span>
</div>
`;

Expand Down
46 changes: 46 additions & 0 deletions src/components/Inputs/TagInput/TagInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.tag-input-container {
width: 100%;

.tag-inline-container {
width: 100%;
border-radius: 2px;

display: flex;
flex-flow: wrap;

background-color: #f2f2f2;
color: black;

padding-left: 10px;
padding-right: 5px;
padding-top: 8px;
padding-bottom: 3px;
}

.tag-input-wrapper {
width: auto;
height: 22px;

flex: 1 1 60px;
display: flex;
justify-content: left;
align-items: center;
margin-bottom: 7px;

.tag-input {
border: 0;
width: 100%;
height: 100%;
background-color: inherit;
color: black;
padding-right: 5px;

&:-ms-input-placeholder,
&:-moz-placeholder,
&::-webkit-input-placeholder,
&::-moz-placeholder {
font-size: 13px;
}
}
}
}
Empty file.
171 changes: 171 additions & 0 deletions src/components/Inputs/TagInput/TagInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import Component from "../../../core/Component/Component";
import { Range } from "../../../lib/Range/Range";
import TagBlock from "../../Tag/TagBlock/TagBlock";
import "./TagInput.scss";

interface TagInputOptions {
onFocus?: () => void;
onFocusout?: () => void;
}

export default class TagInput extends Component {
private readonly onFocus: () => void;
private readonly onFocusout: () => void;
private readonly MAX_TEXT_LENGTH: number = 10;

private inputElement: HTMLInputElement | null = null;
private isPressingBackspace: boolean = false;

constructor({
onFocus = () => {},
onFocusout = () => {},
}: TagInputOptions = {}) {
super({ classNames: ["tag-input-container"] });

this.onFocus = onFocus;
this.onFocusout = onFocusout;

this.store.setDefaultState("tags", []);
this.store.setAction("addTag", ({ state, payload }) => {
const { newTag } = payload;
return { tags: [...state.tags, newTag] };
});
this.store.setAction("removeTag", ({ state, payload }) => {
const { removeId } = payload;
return {
tags: state.tags.filter((aTagBlock: TagBlock) => {
return aTagBlock.id !== removeId;
}),
};
});

this.render();
}

private isValidTextLength(text: string): boolean {
return new Range(text.trim().length)
.moreThan(0)
.equalAndLessThan(this.MAX_TEXT_LENGTH)
.isIn();
}

private submitTag(e: Event) {
e.preventDefault();

if (!this.inputElement) return;
if (this.inputElement.value === "") return;
if (!this.isValidTextLength(this.inputElement.value)) return;

this.store.dispatch("addTag", {
newTag: new TagBlock(this.inputElement.value),
});

this.inputElement.value = "";
this.inputElement.focus();
}

private renderInputElement(): HTMLInputElement {
this.inputElement = document.createElement("input");
this.inputElement.classList.add("tag-input");
this.inputElement.placeholder = "태그 추가...";
this.inputElement.spellcheck = false;
this.inputElement.maxLength = this.MAX_TEXT_LENGTH;

this.inputElement.addEventListener("focus", () => {
this.onFocus();
});
this.inputElement.addEventListener("focusout", () => {
if (this.store.getState("tags").length > 0) return;

this.onFocusout();
});

return this.inputElement;
}

private renderTagInputWrapper(): HTMLElement {
const tagInputWrapper: HTMLFormElement = document.createElement("form");
tagInputWrapper.classList.add("tag-input-wrapper");
tagInputWrapper.addEventListener("submit", this.submitTag.bind(this));
tagInputWrapper.appendChild(this.renderInputElement());

return tagInputWrapper;
}

private handleRemovingTagByClick(e: Event) {
e.stopPropagation();

const clickElement = e.target as HTMLElement;
const tagElement: HTMLElement = clickElement.parentElement!;

if (clickElement.tagName !== "I") return;

this.store.dispatch("removeTag", {
removeId: tagElement.id,
});

tagElement.remove();
this.inputElement!.focus();
}

private handleRemoveingTagByKey(e: KeyboardEvent) {
e.stopPropagation();
if (e.key !== "Backspace") return;
if (this.inputElement!.value !== "") return;
if (this.isPressingBackspace) return;

const tags: Array<TagBlock> = this.store.getState("tags");
const latestTag: TagBlock = tags[tags.length - 1];

this.store.dispatch("removeTag", {
removeId: latestTag.id,
});

latestTag.container.remove();
this.inputElement!.focus();
this.isPressingBackspace = true;
}

private handleInitializeKeyboardState(e: KeyboardEvent) {
e.stopPropagation();
if (e.key !== "Backspace") return;

this.isPressingBackspace = false;
}

private applyEventsForRemoveTag() {
const tagInlineContainer: HTMLDivElement = this.qs(
".tag-inline-container"
)! as HTMLDivElement;

tagInlineContainer.addEventListener(
"click",
this.handleRemovingTagByClick.bind(this)
);
tagInlineContainer.addEventListener(
"keydown",
this.handleRemoveingTagByKey.bind(this)
);
tagInlineContainer.addEventListener(
"keyup",
this.handleInitializeKeyboardState.bind(this)
);
}

render() {
this.container.innerHTML = `
<div class="tag-inline-container"></div>
`;

const tags: Array<TagBlock> = this.store.getState("tags");
this.appendElementsTo(
".tag-inline-container",
...tags.map((aTagBlock) => {
return aTagBlock.container;
}),
this.renderTagInputWrapper()
);

this.applyEventsForRemoveTag();
}
}
6 changes: 3 additions & 3 deletions src/components/Inputs/TextInput/TextInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ export default class TextInput extends Component {
return this.inputElement.value;
}

isValidTextLength(text: string): boolean {
private isValidTextLength(text: string): boolean {
return new Range(text.length).moreThan(0).equalAndLessThan(20).isIn();
}

addInputElementEvents() {
private addInputElementEvents() {
this.inputElement.addEventListener("focus", () => {
this.inputElement.classList.add("focused");
this.placeholderElement.classList.add("focused");
Expand Down Expand Up @@ -72,7 +72,7 @@ export default class TextInput extends Component {
});
}

addPlaceholderElementEvents() {
private addPlaceholderElementEvents() {
this.placeholderElement.addEventListener("click", (e: Event) => {
e.stopPropagation();
this.inputElement.focus();
Expand Down
28 changes: 25 additions & 3 deletions src/components/MainContainer/MainContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import HeadInfoCard from "../Cards/HeadInfoCard/HeadInfoCard";
import InfoCard from "../Cards/InfoCard/InfoCard";
import { executeAnimation } from "../Cards/InfoCard.ani";
import TextInput from "../Inputs/TextInput/TextInput";
import TagInput from "../Inputs/TagInput/TagInput";

export default class MainContainer extends Component {
constructor() {
super({ id: "main-container" });
this.render();
}

renderChilds(): Array<Component> {
private renderChilds(): Array<Component> {
const titleContainerSelector: string = ".card-title-container";
const titleSelector: string = `${titleContainerSelector} > .title`;
const subTitleSelector: string = `${titleContainerSelector} > .sub-title`;
Expand All @@ -29,9 +30,30 @@ export default class MainContainer extends Component {
nameCard.qs(titleSelector)!.classList.remove("hidden");
nameCard.qs(subTitleSelector)!.classList.remove("hidden");
},
})
}),
{
title: "이름은 무엇인가요?",
subTitle: "20자 이내에서 알려주세요!",
}
);
const tagCard = new InfoCard(
new TagInput({
onFocus: () => {
tagCard.qs(titleContainerSelector)!.classList.add("hidden");
tagCard.qs(titleSelector)!.classList.add("hidden");
tagCard.qs(subTitleSelector)!.classList.add("hidden");
},
onFocusout: () => {
tagCard.qs(titleContainerSelector)!.classList.remove("hidden");
tagCard.qs(titleSelector)!.classList.remove("hidden");
tagCard.qs(subTitleSelector)!.classList.remove("hidden");
},
}),
{
title: "관심사도 자유롭게 알려주세요!",
subTitle: "관심사들을 5개까지 태그로 만들어드릴게요.",
}
);
const tagCard = new InfoCard(null);

return [headCard, nameCard, tagCard];
}
Expand Down
18 changes: 18 additions & 0 deletions src/components/Tag/TagBlock/TagBlock.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.tag-block {
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 5px;
margin-bottom: 5px;
padding: 3px 10px;
border-radius: 3px;
line-break: anywhere;
font-size: 13px;
height: 22px;
background-color: teal;

i {
cursor: pointer;
margin-left: 10px;
}
}
5 changes: 5 additions & 0 deletions src/components/Tag/TagBlock/TagBlock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// describe("Test TagBlock: Parse hash text.", () => {
// it("Case 1.", () => {

// })
// });
44 changes: 44 additions & 0 deletions src/components/Tag/TagBlock/TagBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Component from "../../../core/Component/Component";
import "./TagBlock.scss";

interface TagBlockOptions {
hasCloseButton?: boolean;
}

export default class TagBlock extends Component {
readonly text: string;
private readonly hasCloseButton: boolean;

constructor(text: string, { hasCloseButton = true }: TagBlockOptions = {}) {
const { hash, salt }: { hash: string; salt: string } = text.getHash();
super({ id: hash + salt, classNames: ["tag-block"] });

this.hasCloseButton = hasCloseButton;
this.text = this.parseHashText(text);

this.render();
}

private parseHashText(text: string): string {
text = text.trim();
text = text.replaceAll(" ", "_");

if (text.startsWith("#")) return text;
return `#${text}`;
}

setRandomBackgroundColor() {
this.container.style.backgroundColor = `hsl(${Math.floor(
Math.random() * 360
)}, 60%, 75%)`;
}

render() {
this.container.innerHTML = `
<span class="tag-text">${this.text}</span>
${this.hasCloseButton ? "<i class='fa-solid fa-xmark'></i>" : ""}
`;

this.setRandomBackgroundColor();
}
}
Loading

0 comments on commit 1c682c6

Please sign in to comment.