T2Editor Ver 3.0.2 (그누보드5 간편 에디터) (그누보드5 플러그인)
본문
T2Editor
T2Editor는 그누보드5(Gnuboard5)를 위해 설계된 WYSIWYG 웹 에디터입니다. 이 에디터는 사용자 친화적인 인터페이스와 함께 강력한 텍스트 편집 기능을 제공하며, 특히 모바일 환경에서의 사용성을 고려하여 개발되었습니다.
작동화면:
직관적인 사용자 인터페이스 - 누구나 쉽게 사용할 수 있도록 설계된 UI 제공
모바일 환경 최적화 및 높은 호환성 - 안드로이드, iOS를 비롯한 다양한 기기에서 원활한 사용 가능
미디어 미리보기 - 삽입한 이미지 및 동영상 미리보기 지원
이미지 및 동영상 삽입/편집
- 이미지 추가(파일/링크) 및 동영상(유튜브) 삽입/업로드 가능
- 이미지 드레그&드롭 지원 및 멀티 업로드 지원
- 간편 이미지 크기 조절 메뉴 제공
코드 블록 지원 - 각종 프로그래밍 코드 입력 가능
텍스트 링크 기능 - 원하는 텍스트에 하이퍼링크 추가 가능
글꼴 크기 및 색상조절 - 손쉬운 폰트 크기 및 색상 변경
실행 취소/다시 실행 지원 - 작업 중 실수해도 쉽게 복구 가능
자동 저장 기능 - 입력한 내용이 자동으로 저장되어 데이터 손실 방지
자동 글자 수 측정 - 작성한 글자의 개수를 실시간으로 확인 가능
게시물 내보내기 기능 - 작성한 게시물을 html 파일 형태로 내보내기 가능
모듈화 된 구조 - 모듈화된 구조로 일반 사용자도 쉽게 유지보수 가능!
T2Editor ver3.0.2 수정 사항:
코드블럭이 추가되지 않는 문제를 재수정함.
변경사항 주의!
(정정)그누보드5의 게시글 html 필터링으로 인해 본문에 자동으로 추가하는 스타일 파일 링크가 작동하지 않으므로
<link href="<?php echo G5_PLUGIN_URL ?>/editor/t2editor/css/content.css" rel="stylesheet">를 추가해주시길 바랍니다. (1.6.3 -> 3.0.2버전에서 css파일이 t2content.css에서 content.css로 변경됨)
미디어블럭 스타일을 위해
head.sub.php 또는 view.skin.php에 <link href="<?php echo G5_PLUGIN_URL ?>/editor/t2editor/css/t2content.css" rel="stylesheet"> 추가했던 것을 삭제하셔도 됩니다.
(게시글에 자동으로 스타일 파일 link됨)
다운로드
게시글 하단의 첨부파일에서 다운로드하세요.
라이선스
T2Editor은 그누보드5의 발전을 위하여 코드를 공개합니다.
아래의 사항만 지킨다면 누구나 자유롭게 배포할 수 있습니다.
1. 개인(또는 사업자 자체) 사용을 위한 코드 수정 허용
2. 자체 웹사이트 사용을 위한 수정 허용
3. 수정 버전의 배포/공개 시 무료 오픈소스로 배포 필수
3. 원본 및 수정버전의 상업적 유료 배포 불가
설치방법
그누보드5 환경
1. /plugin/editor/ 디렉토리에 첨부파일의 압축을 풀어 (압축 해제한)폴더 안의 t2editor을 업로드 합니다.
2. 관리자 페이지 - 환경설정 > 기본환경설정으로 이동, 에디터 선택 항목에서 t2editor를 선택합니다. 또는 관리자 페이지 - 게시판관리 > 게시판관리 에서 원하는 게시판의 수정 버튼을 눌러 게시판 수정 페이지의 게시판 에디터 선택 항목에서 t2editor를 선택합니다.
3. 미디어블럭 스타일을 위해
head.sub.php 또는 view.skin.php에 <link href="<?php echo G5_PLUGIN_URL ?>/editor/t2editor/css/content.css" rel="stylesheet">를 추가해주세요
T2Editor Ver7.1 업데이트 이후 추가 필요 없음 (자동으로 게시글에 스타일이 link됨) ...(그누보드5 html 필터링 시스템으로 소용 없어짐)
다른 플랫폼 환경
직접 폼 제출 관련 코드를 구현해야 합니다.
<?php
include 't2_config.php';
?>
<link href="./css/core.css" rel="stylesheet">
<link href="./css/dark.css" rel="stylesheet" id="t2editor-dark-css">
<!-- Material Icons (Android 환경 고려) -->
<script>
if (navigator.userAgent.includes('Android')) {
document.write('<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">');
document.write('<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">');
}
</script>
<script>
// T2Editor 설정
const T2EDITOR_URL = './';
const t2editor_url = T2EDITOR_URL;
</script>
<!-- 다크모드 초기화 스크립트 -->
<script>
(function() {
// 다크모드 설정 옵션
const T2EditorConfig = {
enableDarkModeButton: true, // 다크모드 버튼 활성화 여부
forcedTheme: null // 강제 테마 설정 (null: 강제 설정 없음, 'light': 라이트모드 강제, 'dark': 다크모드 강제)
};
if (!T2EditorConfig.enableDarkModeButton && T2EditorConfig.forcedTheme) {
document.documentElement.setAttribute('data-t2editor-theme', T2EditorConfig.forcedTheme);
localStorage.setItem('t2editor-dark-mode', T2EditorConfig.forcedTheme === 'dark');
} else {
var isDarkMode = localStorage.getItem('t2editor-dark-mode') === 'true';
if (isDarkMode === null) {
isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
if (isDarkMode) {
document.documentElement.setAttribute('data-t2editor-theme', 'dark');
} else {
document.documentElement.setAttribute('data-t2editor-theme', 'light');
}
}
document.addEventListener('DOMContentLoaded', function() {
var darkModeToggle = document.querySelector('.t2-dark-mode-toggle');
if (darkModeToggle) {
darkModeToggle.style.display = T2EditorConfig.enableDarkModeButton ? 'flex' : 'none';
}
});
})();
</script>
<!-- Material Icons 폰트 로딩 -->
<style>
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url("./fonts/material-icons/MaterialIcons-Regular.eot");
src: local("Material Icons"),
url("./fonts/material-icons/MaterialIcons-Regular.woff2") format("woff2"),
url("./fonts/material-icons/MaterialIcons-Regular.woff") format("woff"),
url("./fonts/material-icons/MaterialIcons-Regular.ttf") format("truetype");
font-display: swap;
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-feature-settings: "liga";
font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
}
@font-face {
font-family: "Material Icons Outlined";
font-style: normal;
font-weight: 400;
src: url("./fonts/material-icons/MaterialIconsOutlined-Regular.woff2") format("woff2"),
url("./fonts/material-icons/MaterialIconsOutlined-Regular.ttf") format("truetype");
font-display: swap;
}
.material-icons-outlined {
font-family: "Material Icons Outlined";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: "liga";
font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
</style>
<div class="t2-editor-container" id="content_container">
<div class="t2-toolbar">
<button class="t2-btn" data-command="undo" disabled>
<span class="material-icons">undo</span>
</button>
<button class="t2-btn" data-command="redo" disabled>
<span class="material-icons">redo</span>
</button>
<button class="t2-btn" data-command="fontSize">
<span class="material-icons">format_size</span>
</button>
<button class="t2-btn" data-command="bold">
<span class="material-icons">format_bold</span>
</button>
<button class="t2-btn" data-command="italic">
<span class="material-icons">format_italic</span>
</button>
<button class="t2-btn" data-command="underline">
<span class="material-icons">format_underlined</span>
</button>
<button class="t2-btn" data-command="strikeThrough">
<span class="material-icons">format_strikethrough</span>
</button>
<button class="t2-btn" data-command="justifyContent">
<span class="material-icons">format_align_left</span>
</button>
<button class="t2-btn" data-command="foreColor">
<span class="material-icons">format_color_text</span>
</button>
<button class="t2-btn" data-command="backColor">
<span class="material-icons">format_color_fill</span>
</button>
<button class="t2-btn" data-command="insertImage">
<span class="material-icons">image</span>
</button>
<button class="t2-btn" data-command="insertYouTube" style="color:#f04f48">
<span class="material-icons">smart_display</span>
</button>
<button class="t2-btn" data-command="attachFile">
<span class="material-icons">attach_file</span>
</button>
<button class="t2-btn" data-command="createLink">
<span class="material-icons">link</span>
</button>
<button class="t2-btn" data-command="insertTable">
<span class="material-icons-outlined">table_chart</span>
</button>
<button class="t2-btn" data-command="insertCodeBlock">
<span class="material-icons">code</span>
</button>
<button class="t2-btn" data-command="exportHTML">
<span class="material-icons-outlined">ios_share</span>
</button>
</div>
<div class="t2-editor" contenteditable="true" id="content_editor"></div>
<textarea name="content" id="content" style="display:none;"></textarea>
<div class="t2-editor-status">
<div class="t2-status-left">
<div class="t2-logo">
<span class="t2-logo-prefix">T2</span>
<span class="t2-logo-suffix">Editor</span>
</div>
</div>
<!-- 다크모드 토글 -->
<div class="t2-dark-mode-toggle">
<button type="button" class="t2-dark-mode-btn" onclick="toggleT2EditorTheme(event)">
<span class="material-icons t2-dark-mode-icon">dark_mode</span>
<span class="material-icons t2-light-mode-icon">light_mode</span>
</button>
</div>
<div class="t2-char-count">
txt: <span>0</span>
</div>
</div>
#999; position: absolute; right: 5px; margin:5px 0; font-size: 11px; font-weight: 500; display: flex; align-items: center;">
<i class="material-icons-outlined" style="margin-right: 4px; font-size: 14px">info</i>
T2Editor Ver 7.1
</span>
</div>
<!-- T2Editor JavaScript -->
<script src="./js/utils.js"></script>
<script src="./js/core.js"></script>
<!-- T2Editor 플러그인 -->
<script src="./js/plugin/image.js"></script>
<script src="./js/plugin/video.js"></script>
<script src="./js/plugin/file.js"></script>
<script src="./js/plugin/table.js"></script>
<script src="./js/plugin/code.js"></script>
<script src="./js/plugin/link.js"></script>
<script src="./js/plugin/export.js"></script>
<script>
// 다크모드 토글 함수
function toggleT2EditorTheme(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
const currentTheme = document.documentElement.getAttribute('data-t2editor-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-t2editor-theme', newTheme);
localStorage.setItem('t2editor-dark-mode', newTheme === 'dark');
}
// 에디터 초기화
(function() {
const editor = new T2Editor(document.getElementById('content_container'));
window.content_editor = editor;
// 초기 컨텐츠가 있다면 로드
const initialContent = document.getElementById('content').value;
if (initialContent) {
try {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = initialContent;
// 중복 줄바꿈 제거
var nodes = Array.from(tempDiv.childNodes);
for (let i = nodes.length - 1; i >= 0; i--) {
let current = nodes[i];
if (current.nodeType === Node.ELEMENT_NODE && current.tagName === 'P' && !current.textContent.trim() && current.querySelector('br')) {
let prev = i > 0 ? nodes[i - 1] : null;
let next = i < nodes.length - 1 ? nodes[i + 1] : null;
if ((prev && prev.classList?.contains('t2-media-block')) ||
(next && next.classList?.contains('t2-media-block'))) {
current.remove();
}
}
}
// 기존 table-responsive 구조를 t2-table-wrapper로 변환
tempDiv.querySelectorAll('.table-responsive').forEach(function(responsiveWrapper) {
var table = responsiveWrapper.querySelector('table');
if (table) {
if (!table.classList.contains('t2-table')) {
table.classList.add('t2-table');
}
var isLargeTable = table.classList.contains('t2-table-large') ||
(table.rows.length > 10 || (table.rows[0] && table.rows[0].cells.length > 10));
if (isLargeTable && !table.classList.contains('t2-table-large')) {
table.classList.add('t2-table-large');
}
var tableWrapper = document.createElement('div');
tableWrapper.className = 't2-table-wrapper';
tableWrapper.contentEditable = false;
responsiveWrapper.parentNode.insertBefore(tableWrapper, responsiveWrapper);
if (isLargeTable) {
var scrollWrapper = document.createElement('div');
scrollWrapper.className = 't2-table-scroll-wrapper';
tableWrapper.appendChild(scrollWrapper);
scrollWrapper.appendChild(table);
} else {
tableWrapper.appendChild(table);
}
responsiveWrapper.remove();
}
});
editor.setContent(tempDiv.innerHTML);
// 초기화 완료 후 처리
setTimeout(function() {
editor.normalizeContent();
// 플러그인들에게 컨텐츠 로드 완료 알림
for (let [name, plugin] of editor.plugins) {
if (plugin.onContentSet) {
plugin.onContentSet(tempDiv.innerHTML);
}
}
}, 100);
} catch (e) {
console.error('에디터 초기화 오류:', e);
}
}
})();
// 폼 제출시 컨텐츠 처리 함수
function getEditorContent() {
var editorContent = document.getElementById('content_editor').innerHTML;
var tempDiv = document.createElement('div');
tempDiv.innerHTML = editorContent;
// 파일 블록 처리
tempDiv.querySelectorAll('.t2-file-block').forEach(function(block) {
const controls = block.querySelector('.t2-media-controls');
if (controls) {
controls.remove();
}
});
// 미디어 블록 처리
tempDiv.querySelectorAll('.t2-media-block').forEach(function(block) {
var container = block.querySelector('div:first-child');
var mediaElement = container.querySelector('iframe, video, img');
if (mediaElement) {
mediaElement.style.width = container.style.width;
if (container.style.height) {
mediaElement.style.height = container.style.height;
}
var controls = block.querySelector('.t2-media-controls');
if (controls) {
controls.remove();
}
}
});
// 테이블 래퍼 및 컨트롤 처리
tempDiv.querySelectorAll('.t2-table-wrapper').forEach(function(wrapper) {
const table = wrapper.querySelector('table');
if (table) {
const controls = wrapper.querySelector('.t2-table-controls');
if (controls) {
controls.remove();
}
const downloadBtn = wrapper.querySelector('.t2-table-download-btn');
if (downloadBtn) {
downloadBtn.remove();
}
const isLargeTable = table.classList.contains('t2-table-large') ||
(table.rows.length > 10 || (table.rows[0] && table.rows[0].cells.length > 10));
const hasScrollWrapper = wrapper.querySelector('.t2-table-scroll-wrapper');
if (isLargeTable || hasScrollWrapper) {
const scrollContainer = document.createElement('div');
scrollContainer.className = 'table-responsive';
scrollContainer.style.cssText = 'display:block; width:100%; overflow-x:auto; -webkit-overflow-scrolling:touch;';
wrapper.parentNode.insertBefore(scrollContainer, wrapper);
if (hasScrollWrapper) {
hasScrollWrapper.parentNode.insertBefore(table, hasScrollWrapper);
hasScrollWrapper.remove();
}
scrollContainer.appendChild(table);
wrapper.remove();
} else {
wrapper.parentNode.insertBefore(table, wrapper);
wrapper.remove();
}
}
});
// 코드 블록 처리
tempDiv.querySelectorAll('.t2-code-block').forEach(function(block) {
const toolbar = block.querySelector('.t2-code-toolbar');
if (toolbar) {
toolbar.remove();
}
});
// 빈 문단 처리
tempDiv.querySelectorAll('p').forEach(function(p) {
if (!p.textContent.trim() && !p.querySelector('img, iframe, video')) {
if (!p.querySelector('br')) {
p.innerHTML = '<br>';
}
}
});
// 컨텐츠 스타일 추가
var contentStyle = '<link href="./css/content.css" rel="stylesheet">';
var finalContent = tempDiv.innerHTML;
if (finalContent.indexOf('t2-media-block') !== -1 || finalContent.indexOf('t2-table') !== -1 || finalContent.indexOf('t2-code-block') !== -1) {
finalContent += contentStyle;
}
document.getElementById('content').value = finalContent;
return finalContent;
}
// 컨텐츠 검증 함수
function validateEditorContent() {
var editorContent = document.getElementById('content_editor').innerHTML;
function hasRealContent(html) {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var textContent = tempDiv.textContent.trim();
if (textContent) return true;
if (tempDiv.querySelector('img, video, iframe')) return true;
if (tempDiv.querySelector('.t2-file-block, .file-container')) return true;
if (tempDiv.querySelector('table')) return true;
return false;
}
if (!hasRealContent(editorContent)) {
alert('내용을 입력해 주십시오.');
document.getElementById('content_editor').focus();
return false;
}
return true;
}
</script>
필수 환경
PHP 7.x ~
GD Livrary
기여
펄스나인(false9)님의 IOS/Safari에서 발생하는 IME, 엔터키 문제 해결 방법 제시.
jihan006(jihan006)님의 IOS의 IME 문제 해결 관련 자료 제공.
푸른산타(bodr)님의 관리자 에러 해결
*이 게시물은 T2Editor로 작성되었습니다.
첨부파일
카테고리 분류 학습 시스템 (총 0개 학습됨)
이 분류가 맞나요? 학습시켜주세요!
등록된 댓글이 없습니다.