diff --git a/package.json b/package.json index d4d8bad..7ae3fdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dragon-editor", - "version": "2.0.0-beta", + "version": "2.0.0", "description": "WYSIWYG editor on Nuxt.js", "repository": { "type": "git", diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 7eb55f5..1f3a521 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -13,7 +13,18 @@ import { ref, onMounted } from "#imports"; const editor = ref(); -const contentData = ref([]); +const contentData = ref([ + { + type: "ol", + id: "ksadgjkl3", + childList: [ + { + classList: [], + content: "1233333", + }, + ], + }, +]); const option = { // blockMenu: ["text", "ol", "ul"], customBlockMenu: [ @@ -48,7 +59,7 @@ function addImage2() { }); } -function test(){ +function test() { editor.value.dataUpdateAction(); } diff --git a/playground/pages/viewer.vue b/playground/pages/viewer.vue index a91fc6a..0504255 100644 --- a/playground/pages/viewer.vue +++ b/playground/pages/viewer.vue @@ -18,6 +18,20 @@ const contentData = ref([ { type: "text", id: "D3ubnFyQIjYfI9YWs4x6", classList: [], content: "" }, { type: "image", id: "FgCpjlXNZVx7KgHKjEzO", classList: ["d-align-center", "--10"], src: "https://c.pxhere.com/images/37/e4/22c0adf08932049eb1b8af36cbd7-1622414.jpg!d", width: 1200, height: 1739, webp: false, caption: undefined }, { type: "text", id: "0ygjWGTB4V6O2dAOgkPJ", classList: [], content: 'sadfasdfsadfsdfsadfsafsafsafsafsafsafasd' }, + { type: "ol", id: "ksadgjkl3", classList: [], childList: [{ classList: [], content: "1233333" }] }, + { + type: "ul", + id: "YyocAhurpA4eQ5fAxlTe", + classList: [], + childList: [ + { classList: [], content: "sdafsfksdfjkaslfj" }, + { classList: [], content: "saklfjsaldfjsklafs" }, + { classList: [], content: "skalfjsaklfjsadklfsafj" }, + { classList: [], content: "afkjsladf" }, + { classList: [], content: "jsadklfjsakldf" }, + ], + }, + { type: "text", id: "B1akIdmWYW7vshudsRMN", classList: [], content: "" }, ]); diff --git a/src/runtime/core/components/editor/OlBlock.vue b/src/runtime/core/components/editor/OlBlock.vue index 06842de..b78ea6e 100644 --- a/src/runtime/core/components/editor/OlBlock.vue +++ b/src/runtime/core/components/editor/OlBlock.vue @@ -8,7 +8,7 @@ // @ts-ignore import { ref, unref } from "#imports"; import { cursorSelection, liItem, listBlock, styleFunctionArgument } from "../../../../types"; -import { getArrangementCursorData, setCursor, pasteText, styleSettings, keyboardEvent } from "../../utils"; +import { getArrangementCursorData, setCursor, pasteText, styleSettings, keyboardEvent, getCursor, findEditableElement } from "../../utils"; const updateCount = ref(0); const $ol = ref(); @@ -37,7 +37,7 @@ if (data.value.childList.length === 0) { // 키보드 이벤트 할당 function textKeyboardEvent(e: KeyboardEvent) { - keyboardEvent("ol", e, emit, updateBlockData); + keyboardEvent("list", e, emit, updateBlockData); } /** @@ -49,62 +49,71 @@ function updateBlockData() { const $block = $ol.value; const $childList = $block.querySelectorAll("li"); const childData: liItem[] = []; + const cursorData = getCursor(); $childList.forEach((row) => { - console.log(row); + row.childNodes.forEach((child: ChildNode) => { + const $child = child as HTMLElement; + + if (child.constructor.name !== "Text") { + // 텍스트가 아닐경우 + if (child.constructor.name !== "HTMLBRElement") { + // br 태그 유지 + if (child.textContent === "") { + // 빈 태그 삭제 + child.remove(); + } else if ($child.classList.length === 0) { + // 클레스 없는 엘리먼트 처리 + $child.insertAdjacentHTML("afterend", $child.innerHTML); + child.remove(); + } + } else { + $child.removeAttribute("class"); + } + } + }); + childData.push({ - classList: [], + classList: [...row.classList].splice(1), content: row.innerHTML, }); }); - // console.log($block); - // console.log("$childList", $childList); data.value.childList = childData; emit("update:modelValue", data.value); updateCount.value += 1; - // const blockClassList = [...$block.value.classList]; - // blockClassList.splice(0, 1); - // const pushList = blockClassList.filter((item) => data.value.classList.indexOf(item) === -1); - // data.value.classList = data.value.classList.concat(pushList); - // // 클레스 검수 - // const checkClassIdx = data.value.classList.indexOf("d-text-block"); - // if (checkClassIdx > -1) { - // data.value.classList.splice(checkClassIdx, 1); - // } - // // 커서위치 재지정 - // if ($block.value.innerHTML.length > 0) { - // const cursorData = getArrangementCursorData(props.cursorData); - // data.value.content = $block.value.innerHTML; - // emit("update:modelValue", data.value); - // setTimeout(() => { - // if ($block.value) { - // setCursor($block.value.childNodes[cursorData.childCount], cursorData.length); - // // 구조 검수 - // $block.value.childNodes.forEach((child: ChildNode) => { - // const $child = child as HTMLElement; - // if (child.constructor.name !== "Text") { - // // 텍스트가 아닐경우 - // if (child.constructor.name !== "HTMLBRElement") { - // // br 태그 유지 - // if (child.textContent === "") { - // // 빈 태그 삭제 - // child.remove(); - // } else if ($child.classList.length === 0) { - // // 클레스 없는 엘리먼트 처리 - // $child.insertAdjacentHTML("afterend", $child.innerHTML); - // child.remove(); - // } - // } else { - // $child.removeAttribute("class"); - // } - // } - // }); - // } - // }, 100); - // } else { - // emit("update:modelValue", data.value); - // } + + if (cursorData.startNode) { + const editableNode = findEditableElement(cursorData.startNode); + let childIdx = -1; + + $childList.forEach((row, count) => { + if (row === editableNode) { + childIdx = count; + } + }); + + if (childIdx > -1) { + // 기본 로직 + itemIdx.value = childIdx; + setTimeout(() => { + const afterChildList = $ol.value.querySelectorAll("li"); + setCursor(afterChildList[childIdx], cursorData.startOffset as number); + }, 100); + } else { + // 중간 엔터 + $childList.forEach((row, count) => { + if (row === (cursorData.startNode as Node).parentNode) { + childIdx = count; + } + }); + + setTimeout(() => { + const afterChildList = $ol.value.querySelectorAll("li"); + setCursor(afterChildList[childIdx], cursorData.startOffset as number); + }, 100); + } + } } // 포커스 diff --git a/src/runtime/core/components/editor/TextBlock.vue b/src/runtime/core/components/editor/TextBlock.vue index db71c9d..d907b4a 100644 --- a/src/runtime/core/components/editor/TextBlock.vue +++ b/src/runtime/core/components/editor/TextBlock.vue @@ -117,7 +117,13 @@ function updateBlockData() { // 포커스 function focus(type: string = "first") { if (type === "first") { - setCursor($block.value, 0); + if ($block.value.childNodes.length > 0) { + setTimeout(() => { + setCursor($block.value.childNodes[0], 0); + }, 100); + } else { + setCursor($block.value, 0); + } } else { const childCount = $block.value.childNodes.length; const targetChild = $block.value.childNodes[childCount - 1]; diff --git a/src/runtime/core/components/editor/UlBlock.vue b/src/runtime/core/components/editor/UlBlock.vue new file mode 100644 index 0000000..7131415 --- /dev/null +++ b/src/runtime/core/components/editor/UlBlock.vue @@ -0,0 +1,162 @@ + + + diff --git a/src/runtime/core/style/common.css b/src/runtime/core/style/common.css index f87c44d..e9fd2a5 100644 --- a/src/runtime/core/style/common.css +++ b/src/runtime/core/style/common.css @@ -168,6 +168,23 @@ content: "Write text"; color: #ccc; } +.dragon-editor .d-ul-block { + padding-left: 30px; + cursor: text; + list-style: disc; +} +.dragon-editor .d-ul-block .d-li-item { + list-style: inherit; + outline: 0; +} +.dragon-editor .d-ul-block .d-li-item:empty { + min-height: 1.6em; +} +.dragon-editor .d-ul-block .d-li-item:empty::after { + display: inline; + content: "Write text"; + color: #ccc; +} .dragon-editor .d-image-block { display: flex; flex-direction: column; diff --git a/src/runtime/core/style/viewer.css b/src/runtime/core/style/viewer.css index f9ef2e5..8fb4e86 100644 --- a/src/runtime/core/style/viewer.css +++ b/src/runtime/core/style/viewer.css @@ -189,3 +189,17 @@ color: #ccc; font-size: 1rem; } +.dragon-editor-viewer .d-ol-block { + padding-left: 30px; + list-style: decimal; +} +.dragon-editor-viewer .d-ol-block .d-li-item { + list-style: inherit; +} +.dragon-editor-viewer .d-ul-block { + padding-left: 30px; + list-style: disc; +} +.dragon-editor-viewer .d-ul-block .d-li-item { + list-style: inherit; +} diff --git a/src/runtime/core/utils/convertor.ts b/src/runtime/core/utils/convertor.ts new file mode 100644 index 0000000..ad19f3e --- /dev/null +++ b/src/runtime/core/utils/convertor.ts @@ -0,0 +1,56 @@ +// @ts-nocheck +import { editorContentType } from "../../../types"; + +export function convertToHTML(data: editorContentType) { + let htmlStructure = ""; + + data.forEach((row) => { + // TODO : amp 서포트...? + switch (row.type) { + case "text": + htmlStructure += `

${row.content}

`; + break; + case "image": + htmlStructure += ` +
+
+ ${row.caption} +
+ `; + + if (row.caption) { + htmlStructure += `

${row.caption}

`; + } + + htmlStructure += `
`; + break; + case "ol": + htmlStructure += `
    `; + + row.childList.forEach((child) => { + htmlStructure += `
  1. ${child.content}
  2. `; + }); + + htmlStructure += `
`; + break; + case "ul": + htmlStructure += ``; + break; + default: + htmlStructure += row.innerHTML; + } + }); + + //
    + //
  1. + //
+ // + + return htmlStructure; +} diff --git a/src/runtime/core/utils/cursor.ts b/src/runtime/core/utils/cursor.ts index a6964f3..dba5a7a 100644 --- a/src/runtime/core/utils/cursor.ts +++ b/src/runtime/core/utils/cursor.ts @@ -1,7 +1,8 @@ import type { cursorSelection, arrangementCursorData } from "../../../types"; import { findEditableElement } from "./element"; -export function setCursor(target: Node, idx: number) { // 노드 기준 커서 위치 설정 +export function setCursor(target: Node, idx: number) { + // 노드 기준 커서 위치 설정 if (target) { let $target: Node; @@ -26,7 +27,8 @@ export function setCursor(target: Node, idx: number) { // 노드 기준 커서 } } -export function getCursor(): cursorSelection { // 실행 시점 커서 위치 정보값 전달 +export function getCursor(): cursorSelection { + // 실행 시점 커서 위치 정보값 전달 const select = window.getSelection() as Selection; return { @@ -38,17 +40,20 @@ export function getCursor(): cursorSelection { // 실행 시점 커서 위치 }; } -export function getArrangementCursorData(parentCursorData): arrangementCursorData { // Text 노드 병합 전에 병합 후 커서 위치 연산 +export function getArrangementCursorData(parentCursorData): arrangementCursorData { + // Text 노드 병합 전에 병합 후 커서 위치 연산 let cursorData = getCursor(); - if (cursorData.startNode === null) { // 커서위치가 올바르지 않은경우 부모의 커서 위치 사용 + if (cursorData.startNode === null) { + // 커서위치가 올바르지 않은경우 부모의 커서 위치 사용 cursorData = parentCursorData; } let startNode = cursorData.startNode as Node; let editableElement = findEditableElement(startNode) as HTMLElement; - if (editableElement === null) { // 에디터블 노드가 없는 경우 부모의 커서 위치를 사용해 재지정 + if (editableElement === null) { + // 에디터블 노드가 없는 경우 부모의 커서 위치를 사용해 재지정 cursorData = parentCursorData; startNode = cursorData.startNode as Node; editableElement = findEditableElement(startNode) as HTMLElement; @@ -103,5 +108,5 @@ export function getArrangementCursorData(parentCursorData): arrangementCursorDat editableNode: editableElement, childCount: childIdx, length: childLength, - } + }; } diff --git a/src/runtime/core/utils/index.ts b/src/runtime/core/utils/index.ts index 65520d1..8bde3f5 100644 --- a/src/runtime/core/utils/index.ts +++ b/src/runtime/core/utils/index.ts @@ -59,12 +59,11 @@ function createImageBlock(data): imageBlock { src: data.src, width: data.width, height: data.height, - webp: data.webp, caption: data.caption, }; } -// 순서 있는 리스트 블럭 생성 +// 리스트 블럭 생성 function createlistBlock(type: string = "ul"): listBlock { return { type: type, @@ -84,6 +83,9 @@ export function createBlock(name: string, value?: object): allBlock { let blockData: allBlock; switch (name) { + case "ul": + blockData = createlistBlock(); + break; case "ol": blockData = createlistBlock("ol"); break; @@ -102,3 +104,4 @@ export * from "./keyboard"; export * from "./cursor"; export * from "./style"; export * from "./element"; +export * from "./convertor"; diff --git a/src/runtime/core/utils/keyboard.ts b/src/runtime/core/utils/keyboard.ts index f499df8..d44e801 100644 --- a/src/runtime/core/utils/keyboard.ts +++ b/src/runtime/core/utils/keyboard.ts @@ -10,7 +10,7 @@ function enterEvent(type: string, event: KeyboardEvent, action: Function, update const useShift = event.shiftKey; switch (type) { - case "ol": + case "list": if (useShift === false) { if (enterCount == 0) { listEnterEvent(action, update); @@ -88,9 +88,16 @@ function listEnterEvent(action: Function, update: Function) { if (editableElement.childNodes.length === 0 || (endChildIdx === editableElement.childNodes.length - 1 && (editableElement.childNodes[endChildIdx].textContent as string).length === endOffset)) { // 맨뒤 엔터인 경우 - editableElement.insertAdjacentHTML("afterend", `
  • `); - setCursor(editableElement.nextSibling as Node, 0); - update(); + if (editableElement.childNodes.length === 0) { + editableElement.remove(); + action("addBlock", { + name: "text", + }); + } else { + editableElement.insertAdjacentHTML("afterend", `
  • `); + setCursor(editableElement.nextSibling as Node, 0); + update(); + } } else { editableElement.childNodes.forEach((child: ChildNode, count: number) => { const text: string = child.textContent as string; @@ -149,10 +156,11 @@ function listEnterEvent(action: Function, update: Function) { }); editableElement.innerHTML = preHTMLStructor; - action("addBlock", { - name: "text", - value: { classList: editableElementClassList, content: nextHTMLStructor }, - }); + editableElement.insertAdjacentHTML("afterend", `
  • ${nextHTMLStructor}
  • `); + setTimeout(() => { + setCursor((editableElement.nextSibling as HTMLElement).childNodes[0], 0); + update(); + }, 100); } } } diff --git a/src/runtime/shared/components/DragonEditor.vue b/src/runtime/shared/components/DragonEditor.vue index 7a31985..7a8aa49 100644 --- a/src/runtime/shared/components/DragonEditor.vue +++ b/src/runtime/shared/components/DragonEditor.vue @@ -84,6 +84,7 @@ import SvgIcon from "../../core/components/SvgIcon.vue"; import textBlock from "../../core/components/editor/TextBlock.vue"; import imageBlock from "../../core/components/editor/ImageBlock.vue"; import olBlock from "../../core/components/editor/OlBlock.vue"; +import ulBlock from "../../core/components/editor/UlBlock.vue"; // 기본 정보 const props = defineProps<{ @@ -92,7 +93,7 @@ const props = defineProps<{ }>(); const modelValue = ref([]); const option = ref({ - blockMenu: ["text", "ol"], + blockMenu: ["text", "ol", "ul"], // blockMenu: ["text", "ol", "ul", "table", "quotation"], // TODO : 다른 블럭 만들기 }); @@ -557,6 +558,9 @@ function setComponentKind(kind: string) { let componentData: any; switch (kind) { + case "ul": + componentData = ulBlock; + break; case "ol": componentData = olBlock; break; diff --git a/src/runtime/shared/components/DragonEditorViewer.vue b/src/runtime/shared/components/DragonEditorViewer.vue index ec6499f..83fcf7b 100644 --- a/src/runtime/shared/components/DragonEditorViewer.vue +++ b/src/runtime/shared/components/DragonEditorViewer.vue @@ -1,25 +1,10 @@