Skip to content

Commit

Permalink
[#43] 사용자는 커스텀한 "CSS 스타일" 탭 창을 볼 수 있다. (#120)
Browse files Browse the repository at this point in the history
* ✨ feat: toobox custom

* 🔨 refactor: svg path 객체 파일 분리

* 🐛 fix: merge 오류 수정

* 🐛 fix: css layer 배치 수정

* ✨ feat: 탭 버튼 추가

* 🔨 refactor: type 없어서 build 안되는 오류 해결

* 💄 design: 워크스페이스 반응형으로 변경

* 🐛 fix: border box 잘못 넣은 것 빼기

* 🐛 fix: html 탭과 첫 번째 카테고리 선택되도록 수정

* 🐛 fix: 탭 한 번 선택되면 다시 눌렀을 때 안 꺼지게

* ✨ feat: 탭 선택 시 선택 해제되지 않도록 수정

* 🔨 refactor: tabbed Toolbox 커스텀 클래스로 분리

* 🔨 refactor: 불필요한 코드 정리

* ✨ feat: flyout 위치 고정

* ✨ feat: div로 변경하는 클래스 생성

* 🐛 fix: null로수정

* 🐛 fix: build 되도록 코드 수정

* 🐛 fix: contentarea로 수정

* ✨ feat: div 안에 svg 넣기

* 🐛 fix: flyout 사이즈 조절

* 🔨 refactor: 인터페이스 이름 변경

* ✨ feat: custom flyout register에 등록해서 가져다 사용하기

* 🐛 fix: 뒤에 요소 여러 개 렌더링 되는 거 해결

* ✨ feat: CSS스타일툴박스에 요소 추가 (input, button)

* 🎨 style: 클래스 내 멤버 변수, 메서드 생성 및 TODO 표시

* 💄 design: 스타일 toolbox의 text, input, button 디자인

* ✨ feat: 스타일 추가시 input 값 초기화 + alert->toast 변경

* 🔨 refactor: 가독성 개선

* 🐛 fix: flyout 스크롤바 생기는 에러 수정

* 🙀 chore: 오타 수정

* 🙀 chore: 테스트 코드 삭제

* ✨ feat: 블록 삭제 기능 추가

* 🎨 style: 로직 순서 변경(블록 생성) 및 타입 추가

* 🎨 style: 스타일 형식 변경 (인라인 -> 클래스에 지정)

* 🔨 refactor: svg 경로 변경 + 코드 가독성 개선

* 🎨 style: 태그 변경(p>label) & DOM 요소명 통일 & 로직 순서 변경(전역상태 예외처리 후 불러오도록 수정)

* 💄 design: 디자인 디테일 수정 (px->rem, 기존 설정 활용, placeholder와 입력값/button hover시 색상차이)

* 🔨 refactor: css스타일블록 삭제 기능 개선

* 🐛 fix: 수정 전 코드 삭제

* 🐛 fix: conflict 재해결

* 🐛 fix: 타입 에러 재수정

* 🚚 rename: 명칭 수정 (스타일>클래스)

* 🚚 rename: 툴박스 명칭 수정 (스타일>클래스)

---------

Co-authored-by: Yujin <[email protected]>
Co-authored-by: Yujin Lee <[email protected]>
Co-authored-by: leeyeongjae <[email protected]>
Co-authored-by: chichoc <[email protected]>
Co-authored-by: chichoc <[email protected]>
  • Loading branch information
6 people authored Nov 25, 2024
1 parent 2ba08f0 commit 60da41a
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 58 deletions.
5 changes: 1 addition & 4 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
chore/* @boostcampwm-2024/Web31
hotfix/* @boostcampwm-2024/Web31

* @boostcampwm-2024/Web31
* @boostcampwm-2024/Web31
38 changes: 38 additions & 0 deletions apps/client/src/app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,41 @@ input[type='color']::-webkit-color-swatch {
input[type='color']::-webkit-color-swatch-wrapper {
padding: 0;
}

.contentCreatingBlock {
display: flex;
flex-direction: column;
padding: 1.25rem 1rem;
}

.creatingBlockLabel {
@apply text-bold-rg text-gray-black;
margin-bottom: 0.75rem;
}

/* TODO: placeholder와 색상 차이 (논의 필요) */
.creatingBlockInput {
@apply text-semibold-rg bg-gray-50 text-gray-400;
border: 1px solid #cdd9e4;
border-radius: 0.5rem;
padding: 0.5rem 1.25rem;
width: 100%;
height: 2.5rem;
}

.creatingBlockInput::placeholder {
@apply text-gray-200;
}

.creatingBlockButton {
@apply text-gray-white bg-blue-500;
margin-top: 0.5rem;
font-size: 1.25rem;
border-radius: 0.5rem;
width: 100%;
height: 2.5rem;
}

.creatingBlockButton:hover {
@apply bg-blue-300;
}
22 changes: 22 additions & 0 deletions apps/client/src/core/fieldClickableImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Blockly from 'blockly/core';

export default class FieldClickableImage extends Blockly.FieldImage {
private clickHandler_: (() => void) | null;

constructor(src: string, width: number, height: number, alt: string, clickHandler: () => void) {
super(src, width, height, alt);
this.clickHandler_ = clickHandler;
}

showEditor_() {
if (this.clickHandler_) {
this.clickHandler_();
}
}

onMouseDown_(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
this.showEditor_();
}
}
157 changes: 108 additions & 49 deletions apps/client/src/core/styleFlyout.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,141 @@
import * as Blockly from 'blockly/core';
import toast from 'react-hot-toast';
import TabbedToolbox from './tabbedToolbox';
import FixedFlyout from './fixedFlyout';
import Dom from './dom';
import { cssStyleToolboxConfig } from '@/widgets';
//import { useClassBlockStore } from '@/shared/store';
//import toast from 'react-hot-toast';
import { useClassBlockStore } from '@/shared/store';
import FieldClickableImage from './fieldClickableImage';
import cssClassDeleteIcon from '@/shared/assets/css_class_delete_icon.svg';
import { Tblock } from '@/shared/types';

export default class StyleFlyout extends FixedFlyout {
static registryName = 'StyleFlyout';

pElement: HTMLDivElement | null = null;
inputElement: HTMLInputElement | null = null;
buttonElement: HTMLButtonElement | null = null;

// flyout 위치 오버라이딩
position(): void {
super.position(); // FixedFlyout의 기본 배치 호출
const toolbox = this.targetWorkspace!.getToolbox();

if (!toolbox) {
throw new Error('no toolbox');
}

const metrics = (toolbox as TabbedToolbox).getContentAreaMetrics();
this.positionAt_(metrics.width - 10, metrics.height - 150, 10, 150);
}

init(targetWorkspace: Blockly.WorkspaceSvg): void {
super.init(targetWorkspace);
const toolbox = this.targetWorkspace.getToolbox() as TabbedToolbox;

const styleTop = Dom.createElement<HTMLDivElement>('div', {
const cssStyleToolboxDivElement = Dom.createElement<HTMLDivElement>('div', {
class: 'contentCreatingBlock',
});

const inputElement = Dom.createElement<HTMLInputElement>('input', {
const labelElement = Dom.createElement<HTMLLabelElement>('label', {
for: 'creatingBlockInput',
class: 'creatingBlockLabel',
});
labelElement.textContent = '클래스명';

this.inputElement = Dom.createElement<HTMLInputElement>('input', {
type: 'text',
placeholder: '스타일명을 정해주세요',
class: 'flyout-input',
placeholder: '클래스명을 정해주세요',
class: 'creatingBlockInput',
id: 'creatingBlockInput',
});
inputElement.style.zIndex = '99999';

const buttonElement = Dom.createElement<HTMLButtonElement>('button', {
class: 'flyout-button',
class: 'creatingBlockButton',
});
buttonElement.textContent = '+';
buttonElement.addEventListener('click', () => this.createStyleBlock());
// TODO: input 입력값 존재 && focus된 경우 Enter 클릭하면 CSS 클래스명 블록 생성

buttonElement.addEventListener('click', () => {
const inputValue = inputElement.value.trim();
// const { addClassBlock } = useClassBlockStore.getState();
[labelElement, this.inputElement, buttonElement].forEach((element) =>
cssStyleToolboxDivElement.appendChild(element)
);

if (inputValue === null) {
return;
}
toolbox.addElementToContentArea(cssStyleToolboxDivElement);
// TODO: toolbox 중복 호출 논의
this.show(cssStyleToolboxConfig.contents);
}

if (inputValue?.trim() === '') {
return alert('블록 이름을 입력해주세요.');
}
createStyleBlock() {
const inputValue = this.inputElement?.value;
if (!inputValue) {
return toast.error('클래스명을 입력해주세요.');
}

if (!Blockly.Blocks[inputValue!]) {
Blockly.Blocks[inputValue!] = {
init: function () {
this.appendDummyInput().appendField(
new Blockly.FieldLabelSerializable(inputValue!),
'CLASS'
); // 입력된 이름 반영
this.setOutput(true);
this.setColour('#02D085');
},
};
}
const existingBlocks: Tblock[] = cssStyleToolboxConfig!.contents || [];
const isBlockAlreadyAdded = existingBlocks.some((block) => block.type === inputValue);
if (isBlockAlreadyAdded) {
return toast.error(`"${inputValue}" 입력한 클래스명 블록은 이미 존재합니다.`);
}

// 기존 블록 유지 및 새 블록 추가
// const existingBlocks = cssStyleToolboxConfig!.contents || [];
// const isBlockAlreadyAdded = existingBlocks.some((block) => block.type === inputValue);

// if (isBlockAlreadyAdded) {
// toast.error(`"${inputValue}" 스타일 블록은 이미 존재합니다.`);
// return;
// }

// if (inputValue) {
// cssStyleToolboxConfig!.contents = [...existingBlocks, { kind: 'block', type: inputValue }];
// addClassBlock(inputValue);
// }
// this.show(cssStyleToolboxConfig.contents);
// toast.success(`새 스타일 블록 "${inputValue}"이(가) 추가되었습니다.`);
});
if (!Blockly.Blocks[inputValue!]) {
const flyoutInstance = this;
Blockly.Blocks[inputValue!] = {
init: function () {
const input = this.appendDummyInput();
input.appendField(new Blockly.FieldLabelSerializable(inputValue!), 'CLASS');

// TODO: CSS 클래스명 블록 색상 변경
input.appendField(
new FieldClickableImage(
cssClassDeleteIcon,
12,
12,
'삭제',
flyoutInstance.deleteStyleBlock.bind(flyoutInstance, inputValue!)
)
);

this.setOutput(true);
this.setColour('#02D085');
// this.setColour('#F4F8FA');
},
};
}

styleTop.appendChild(inputElement);
styleTop.appendChild(buttonElement);
// 기존 블록에 새 블록 추가
cssStyleToolboxConfig!.contents = [...existingBlocks, { kind: 'block', type: inputValue }];
const { addClassBlock } = useClassBlockStore.getState();
addClassBlock(inputValue);

toolbox.addElementToContentArea(styleTop);
this.show(cssStyleToolboxConfig.contents);
toast.success(`입력한 클래스명 블록 "${inputValue}"이(가) 추가되었습니다.`);

if (this.inputElement) {
this.inputElement.value = '';
}
}

createStyleBlock() {}
// TODO: 워크스페이스에 존재하는 CSS 클래스명 블록 삭제 논의 필요
deleteStyleBlock(blockType: string) {
const blocks = this.workspace_.getAllBlocks();

// CSS 클래스명 블록 삭제
for (let i = 0; i < blocks.length; i++) {
if (blocks[i].type === blockType) {
blocks[i].dispose(false, true);
break;
}
}

cssStyleToolboxConfig.contents = cssStyleToolboxConfig.contents.filter(
(block) => block.type !== blockType
);

const { removeClassBlock } = useClassBlockStore.getState();
removeClassBlock(blockType);

this.show(cssStyleToolboxConfig.contents);
toast.success(`"${blockType}" 클래스명 블록이 삭제되었습니다.`);
}
}
7 changes: 7 additions & 0 deletions apps/client/src/shared/assets/css_class_delete_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion apps/client/src/shared/store/useClassBlockStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { create } from 'zustand';

type TclassBlock = {
classBlockList: string[];
addClassBlock: (newClassBlockName: string) => void
addClassBlock: (newClassBlockName: string) => void;
removeClassBlock: (classBlockName: string) => void;
};

export const useClassBlockStore = create<TclassBlock>((set) => ({
Expand All @@ -12,4 +13,9 @@ export const useClassBlockStore = create<TclassBlock>((set) => ({
classBlockList: [...state.classBlockList, newClassBlockName],
}));
},
removeClassBlock: (classBlockName: string) => {
set((state) => ({
classBlockList: state.classBlockList.filter((name) => name !== classBlockName),
}));
},
}));
1 change: 1 addition & 0 deletions apps/client/src/shared/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export type { TcssCategory, TcssCategoryItem, TcssCategoryList } from './cssCate

export type { IExtendedIToolbox } from './extendedType';
export type { TTabConfig, TTabsConfig, TTabToolboxConfig } from './tabType';
export type { Tblock, TtoolboxConfig } from './styleToolboxType';
6 changes: 6 additions & 0 deletions apps/client/src/shared/types/styleToolboxType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Tblock = { kind: string; type: string };

export type TtoolboxConfig = {
kind: string;
contents: Tblock[];
};
2 changes: 2 additions & 0 deletions apps/client/src/shared/utils/tags.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// TODO: 위치 논의 필요 (shared or core)

export const container = ['div', 'span', 'header', 'section', 'nav', 'main', 'article', 'footer'];
export const text = [
'p',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const cssStyleToolboxConfig = {
import { TtoolboxConfig } from '@/shared/types';

export const cssStyleToolboxConfig: TtoolboxConfig = {
kind: 'categoryToolbox',
contents: [],
};

// type: addPreviousTypeName('css_style')
2 changes: 1 addition & 1 deletion apps/client/src/widgets/workspace/blockly/tabConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const tabToolboxConfig: TTabToolboxConfig = {
flyoutRegistryName: FixedFlyout.registryName,
},
css: {
label: 'CSS 스타일',
label: 'CSS 클래스',
toolboxConfig: cssStyleToolboxConfig,
flyoutRegistryName: StyleFlyout.registryName,
},
Expand Down

0 comments on commit 60da41a

Please sign in to comment.