339 lines
13 KiB
JavaScript
339 lines
13 KiB
JavaScript
(() => {
|
|
const gallery = document.querySelector('[data-media-upload="gallery"]');
|
|
if (gallery) {
|
|
const fileInput = gallery.querySelector('[data-media-files]');
|
|
const list = gallery.querySelector('[data-media-list]');
|
|
fileInput?.addEventListener('change', () => {
|
|
list.replaceChildren();
|
|
const title = document.querySelector('input[name="title"]')?.value.trim() || '게시글';
|
|
[...fileInput.files].forEach((file, index) => {
|
|
const item = document.createElement('article');
|
|
item.className = 'admin-new-media-item';
|
|
const preview = document.createElement('img');
|
|
preview.src = URL.createObjectURL(file);
|
|
preview.alt = '';
|
|
const name = document.createElement('strong');
|
|
name.textContent = file.name;
|
|
const label = document.createElement('label');
|
|
label.textContent = '대체 텍스트';
|
|
const alt = document.createElement('input');
|
|
alt.name = 'new_gallery_alts[]';
|
|
alt.value = `${title} - 대표 이미지 ${index + 1}`;
|
|
alt.maxLength = 255;
|
|
alt.required = true;
|
|
label.append(alt);
|
|
item.append(preview, name, label);
|
|
list.append(item);
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.admin-media-delete input[type="checkbox"]').forEach((checkbox) => {
|
|
checkbox.addEventListener('change', () => {
|
|
const item = checkbox.closest('.admin-media-item');
|
|
const alt = item?.querySelector('input:not([type]), input[type="text"]');
|
|
item?.classList.toggle('is-delete', checkbox.checked);
|
|
if (alt) alt.required = !checkbox.checked;
|
|
});
|
|
});
|
|
})();
|
|
|
|
(() => {
|
|
const editor = document.querySelector('[data-block-editor]');
|
|
const sourceWrap = document.querySelector('[data-body-source]');
|
|
const source = document.querySelector('[data-post-body]');
|
|
if (!editor || !sourceWrap || !source) return;
|
|
|
|
const list = editor.querySelector('[data-block-list]');
|
|
const deleteBin = editor.querySelector('[data-delete-bin]');
|
|
const inventory = new Map();
|
|
editor.querySelectorAll('[data-existing-inline]').forEach((item) => {
|
|
inventory.set(Number(item.dataset.token), {
|
|
id: Number(item.dataset.id),
|
|
token: Number(item.dataset.token),
|
|
path: item.dataset.path,
|
|
alt: item.dataset.alt,
|
|
});
|
|
});
|
|
let nextToken = Number(editor.dataset.nextToken || 1);
|
|
|
|
const button = (label, action, className = '') => {
|
|
const element = document.createElement('button');
|
|
element.type = 'button';
|
|
element.textContent = label;
|
|
element.dataset.blockAction = action;
|
|
if (className) element.className = className;
|
|
return element;
|
|
};
|
|
|
|
const resizeText = (textarea) => {
|
|
textarea.style.height = 'auto';
|
|
textarea.style.height = `${Math.max(112, textarea.scrollHeight)}px`;
|
|
};
|
|
|
|
const updateControls = () => {
|
|
const blocks = [...list.children];
|
|
blocks.forEach((block, index) => {
|
|
block.querySelector('[data-block-action="up"]').disabled = index === 0;
|
|
block.querySelector('[data-block-action="down"]').disabled = index === blocks.length - 1;
|
|
});
|
|
};
|
|
|
|
const bindBlockActions = (block) => {
|
|
block.querySelector('[data-block-action="up"]').addEventListener('click', () => {
|
|
const previous = block.previousElementSibling;
|
|
if (previous) list.insertBefore(block, previous);
|
|
updateControls();
|
|
});
|
|
block.querySelector('[data-block-action="down"]').addEventListener('click', () => {
|
|
const next = block.nextElementSibling;
|
|
if (next) list.insertBefore(next, block);
|
|
updateControls();
|
|
});
|
|
block.querySelector('[data-block-action="remove"]').addEventListener('click', () => {
|
|
if (block.dataset.existingId) {
|
|
const deleted = document.createElement('input');
|
|
deleted.type = 'hidden';
|
|
deleted.name = 'delete_images[]';
|
|
deleted.value = block.dataset.existingId;
|
|
deleteBin.append(deleted);
|
|
}
|
|
block.remove();
|
|
if (!list.children.length) addTextBlock('');
|
|
updateControls();
|
|
});
|
|
};
|
|
|
|
const createShell = (type, title) => {
|
|
const block = document.createElement('article');
|
|
block.className = `admin-content-block admin-content-block-${type}`;
|
|
block.dataset.blockType = type;
|
|
const header = document.createElement('header');
|
|
const heading = document.createElement('strong');
|
|
heading.textContent = title;
|
|
const actions = document.createElement('div');
|
|
actions.className = 'admin-content-block-actions';
|
|
actions.append(
|
|
button('위로', 'up'),
|
|
button('아래로', 'down'),
|
|
button('삭제', 'remove', 'is-danger'),
|
|
);
|
|
header.append(heading, actions);
|
|
block.append(header);
|
|
bindBlockActions(block);
|
|
return block;
|
|
};
|
|
|
|
const appendInsertControls = (block) => {
|
|
const controls = document.createElement('div');
|
|
controls.className = 'admin-content-insert-actions';
|
|
const addText = document.createElement('button');
|
|
addText.type = 'button';
|
|
addText.textContent = '아래에 문단 추가';
|
|
addText.addEventListener('click', () => {
|
|
const added = addTextBlock('', block.nextElementSibling);
|
|
added.querySelector('textarea').focus();
|
|
});
|
|
const addImage = document.createElement('button');
|
|
addImage.type = 'button';
|
|
addImage.textContent = '아래에 이미지 추가';
|
|
addImage.addEventListener('click', () => chooseImage(block.nextElementSibling));
|
|
controls.append(addText, addImage);
|
|
block.append(controls);
|
|
};
|
|
|
|
const addTextBlock = (value, before = null) => {
|
|
const block = createShell('text', '텍스트 문단');
|
|
const textarea = document.createElement('textarea');
|
|
textarea.rows = 4;
|
|
textarea.value = value;
|
|
textarea.placeholder = '본문 내용을 입력하세요.';
|
|
textarea.setAttribute('aria-label', '본문 텍스트 문단');
|
|
textarea.addEventListener('input', () => resizeText(textarea));
|
|
block.append(textarea);
|
|
appendInsertControls(block);
|
|
list.insertBefore(block, before);
|
|
requestAnimationFrame(() => resizeText(textarea));
|
|
updateControls();
|
|
return block;
|
|
};
|
|
|
|
const addImageBlock = ({ id = null, token, path, alt, fileInput = null }, before = null) => {
|
|
const block = createShell('image', id ? '등록된 본문 이미지' : '새 본문 이미지');
|
|
block.dataset.token = String(token);
|
|
if (id) block.dataset.existingId = String(id);
|
|
|
|
const preview = document.createElement('img');
|
|
preview.src = path;
|
|
preview.alt = '';
|
|
const fields = document.createElement('div');
|
|
fields.className = 'admin-content-image-fields';
|
|
const label = document.createElement('label');
|
|
label.textContent = '대체 텍스트';
|
|
const altInput = document.createElement('input');
|
|
altInput.name = id ? `media_alt[${id}]` : 'new_inline_alts[]';
|
|
altInput.value = alt;
|
|
altInput.maxLength = 255;
|
|
altInput.required = true;
|
|
label.append(altInput);
|
|
fields.append(label);
|
|
|
|
if (fileInput) {
|
|
fileInput.name = 'inline_images[]';
|
|
fileInput.className = 'admin-hidden-file';
|
|
const tokenInput = document.createElement('input');
|
|
tokenInput.type = 'hidden';
|
|
tokenInput.name = 'new_inline_tokens[]';
|
|
tokenInput.value = String(token);
|
|
fields.append(fileInput, tokenInput);
|
|
}
|
|
block.append(preview, fields);
|
|
appendInsertControls(block);
|
|
list.insertBefore(block, before);
|
|
updateControls();
|
|
return block;
|
|
};
|
|
|
|
const imageCount = () => list.querySelectorAll('[data-block-type="image"]').length;
|
|
const chooseImage = (before = null) => {
|
|
if (imageCount() >= 10) {
|
|
window.alert('본문 이미지는 최대 10장까지 등록할 수 있습니다.');
|
|
return;
|
|
}
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = 'image/jpeg,image/png,image/webp';
|
|
input.className = 'admin-hidden-file';
|
|
input.addEventListener('change', () => {
|
|
const file = input.files?.[0];
|
|
if (!file) return;
|
|
const title = document.querySelector('input[name="title"]')?.value.trim() || '게시글';
|
|
addImageBlock({
|
|
token: nextToken++,
|
|
path: URL.createObjectURL(file),
|
|
alt: `${title} - 본문 이미지`,
|
|
fileInput: input,
|
|
}, before?.isConnected ? before : null);
|
|
}, { once: true });
|
|
deleteBin.append(input);
|
|
input.click();
|
|
};
|
|
|
|
const usedTokens = new Set();
|
|
const initialBody = source.value.trim();
|
|
const parts = initialBody ? initialBody.split(/\r?\n(?:[\t ]*\r?\n)+/) : [];
|
|
parts.forEach((part) => {
|
|
const match = part.trim().match(/^\[\[image:(\d+)\]\]$/);
|
|
const media = match ? inventory.get(Number(match[1])) : null;
|
|
if (media) {
|
|
addImageBlock(media);
|
|
usedTokens.add(media.token);
|
|
} else {
|
|
addTextBlock(part);
|
|
}
|
|
});
|
|
inventory.forEach((media) => {
|
|
if (!usedTokens.has(media.token)) addImageBlock(media);
|
|
});
|
|
if (!list.children.length) addTextBlock('');
|
|
|
|
editor.querySelector('[data-add-text]').addEventListener('click', () => {
|
|
const block = addTextBlock('');
|
|
block.querySelector('textarea').focus();
|
|
});
|
|
editor.querySelector('[data-add-image]').addEventListener('click', () => chooseImage());
|
|
|
|
source.required = false;
|
|
sourceWrap.classList.add('is-enhanced');
|
|
editor.classList.add('is-ready');
|
|
source.closest('form').addEventListener('submit', () => {
|
|
source.value = [...list.children].map((block) => {
|
|
if (block.dataset.blockType === 'image') return `[[image:${block.dataset.token}]]`;
|
|
return block.querySelector('textarea').value.trim();
|
|
}).filter(Boolean).join('\n\n');
|
|
});
|
|
|
|
const previewDialog = document.querySelector('[data-live-preview]');
|
|
const openPreview = document.querySelector('[data-open-live-preview]');
|
|
const closePreview = previewDialog?.querySelector('[data-close-live-preview]');
|
|
const renderPreview = () => {
|
|
const previewTitle = previewDialog.querySelector('[data-preview-title]');
|
|
const previewCategory = previewDialog.querySelector('[data-preview-category]');
|
|
const previewAuthor = previewDialog.querySelector('[data-preview-author]');
|
|
const previewBody = previewDialog.querySelector('[data-preview-body]');
|
|
const previewGallery = previewDialog.querySelector('[data-preview-gallery]');
|
|
const galleryMain = previewDialog.querySelector('[data-preview-gallery-main]');
|
|
const galleryThumbs = previewDialog.querySelector('[data-preview-gallery-thumbs]');
|
|
const galleryCount = previewDialog.querySelector('[data-preview-gallery-count]');
|
|
|
|
previewTitle.textContent = document.querySelector('input[name="title"]')?.value.trim() || '제목 없는 글';
|
|
previewCategory.textContent = document.querySelector('select[name="category"]')?.value || '';
|
|
previewAuthor.textContent = `${document.querySelector('input[name="author"]')?.value.trim() || ''} 기술팀`;
|
|
previewBody.replaceChildren();
|
|
[...list.children].forEach((block) => {
|
|
if (block.dataset.blockType === 'image') {
|
|
const figure = document.createElement('figure');
|
|
const image = document.createElement('img');
|
|
image.src = block.querySelector('img').src;
|
|
image.alt = block.querySelector('.admin-content-image-fields input')?.value.trim() || '';
|
|
figure.append(image);
|
|
previewBody.append(figure);
|
|
return;
|
|
}
|
|
const value = block.querySelector('textarea').value.trim();
|
|
if (!value) return;
|
|
const paragraph = document.createElement('p');
|
|
paragraph.textContent = value;
|
|
previewBody.append(paragraph);
|
|
});
|
|
if (!previewBody.children.length) {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'is-empty';
|
|
empty.textContent = '본문 내용을 입력하면 여기에 표시됩니다.';
|
|
previewBody.append(empty);
|
|
}
|
|
|
|
const galleryItems = [
|
|
...document.querySelectorAll('[data-media-upload="gallery"] .admin-media-item:not(.is-delete), [data-media-upload="gallery"] .admin-new-media-item'),
|
|
].map((item) => ({
|
|
src: item.querySelector('img')?.src || '',
|
|
alt: item.querySelector('input:not([type="checkbox"])')?.value.trim() || '',
|
|
})).filter((item) => item.src);
|
|
galleryMain.replaceChildren();
|
|
galleryThumbs.replaceChildren();
|
|
previewGallery.hidden = galleryItems.length === 0;
|
|
if (galleryItems.length) {
|
|
const mainImage = document.createElement('img');
|
|
mainImage.src = galleryItems[0].src;
|
|
mainImage.alt = galleryItems[0].alt;
|
|
galleryMain.append(mainImage);
|
|
galleryItems.forEach((item, index) => {
|
|
const thumb = document.createElement('button');
|
|
thumb.type = 'button';
|
|
thumb.setAttribute('aria-label', `${index + 1}번째 대표 이미지 보기`);
|
|
if (index === 0) thumb.classList.add('is-active');
|
|
const image = document.createElement('img');
|
|
image.src = item.src;
|
|
image.alt = '';
|
|
thumb.append(image);
|
|
thumb.addEventListener('click', () => {
|
|
mainImage.src = item.src;
|
|
mainImage.alt = item.alt;
|
|
[...galleryThumbs.children].forEach((child) => child.classList.toggle('is-active', child === thumb));
|
|
});
|
|
galleryThumbs.append(thumb);
|
|
});
|
|
galleryCount.textContent = `대표 이미지 ${galleryItems.length}장`;
|
|
}
|
|
};
|
|
|
|
openPreview?.addEventListener('click', () => {
|
|
renderPreview();
|
|
previewDialog.showModal();
|
|
});
|
|
closePreview?.addEventListener('click', () => previewDialog.close());
|
|
previewDialog?.addEventListener('click', (event) => {
|
|
if (event.target === previewDialog) previewDialog.close();
|
|
});
|
|
})();
|