_Editor.cshtml 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <link rel="stylesheet" href="~/lib/ckeditor/browser/ckeditor5.css" asp-append-version="true" />
  2. <script type="module">
  3. import {
  4. ClassicEditor, Essentials, Paragraph, Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript, RemoveFormat, List, TodoList,
  5. Indent, Heading, Font, Highlight, Alignment, Link, Image, ImageToolbar, ImageCaption, ImageStyle, ImageResize, ImageUpload, MediaEmbed, CodeBlock,
  6. HtmlEmbed, SpecialCharacters, HorizontalLine, PageBreak, SourceEditing, FindAndReplace, SelectAll, BlockQuote, Table, TableToolbar, TextPartLanguage
  7. } from "/lib/ckeditor/browser/ckeditor5.js";
  8. class CustomImageAttributesPlugin {
  9. constructor(editor) {
  10. this.editor = editor;
  11. }
  12. init() {
  13. const editor = this.editor;
  14. const schema = editor.model.schema;
  15. // data-* 속성을 추가
  16. const attributes = ['data-name', 'data-extension', 'data-size', 'data-type', 'data-width', 'data-height'];
  17. schema.extend('imageBlock', {allowAttributes: attributes});
  18. schema.extend('imageInline', {allowAttributes: attributes});
  19. editor.plugins.get('ImageUploadEditing').on('uploadComplete', (evt, { data, imageElement }) => {
  20. editor.model.change((writer) => {
  21. writer.setAttribute('data-name', data.name, imageElement);
  22. writer.setAttribute('data-extension', data.extension, imageElement);
  23. writer.setAttribute('data-size', data.size, imageElement);
  24. writer.setAttribute('data-type', data.type, imageElement);
  25. });
  26. });
  27. attributes.forEach(attr => {
  28. editor.conversion.for('upcast').attributeToAttribute({ model: attr, view: attr });
  29. });
  30. attributes.forEach(attr => {
  31. editor.conversion.for('downcast').attributeToAttribute({ model: attr, view: attr });
  32. editor.conversion.for('editingDowncast').attributeToAttribute({ model: attr, view: attr });
  33. });
  34. const updateDataAttribute = (evt, data, conversionApi) => {
  35. const viewWriter = conversionApi.writer;
  36. const viewElement = conversionApi.mapper.toViewElement(data.item);
  37. if (viewElement) {
  38. const imgElement = viewElement.getChild(0);
  39. if (imgElement && imgElement.is('element', 'img')) {
  40. viewWriter.setAttribute(data.attributeKey, data.attributeNewValue, imgElement);
  41. }
  42. }
  43. };
  44. attributes.forEach(attribute => {
  45. editor.conversion.for('downcast').add(dispatcher => {
  46. dispatcher.on(`attribute:${attribute}:imageBlock`, (evt, data, conversionApi) => {
  47. updateDataAttribute(evt, data, conversionApi);
  48. });
  49. });
  50. editor.conversion.for('editingDowncast').add(dispatcher => {
  51. dispatcher.on(`attribute:${attribute}:imageInline`, (evt, data, conversionApi) => {
  52. updateDataAttribute(evt, data, conversionApi);
  53. });
  54. });
  55. });
  56. }
  57. }
  58. class CustomUploadAdapter {
  59. constructor(loader) {
  60. this.loader = loader;
  61. }
  62. upload() {
  63. return this.loader.file.then(file => {
  64. return new Promise((resolve, reject) => {
  65. const reader = new FileReader();
  66. reader.onload = () => {
  67. resolve({
  68. default: reader.result,
  69. name: file.name,
  70. extension: file.name.split('.').pop(),
  71. size: file.size,
  72. type: file.type
  73. });
  74. };
  75. reader.onerror = error => reject(error);
  76. reader.readAsDataURL(file);
  77. });
  78. });
  79. }
  80. abort() {
  81. console.log('업로드가 취소되었습니다.');
  82. }
  83. }
  84. // CKEditor 저장, 전역 변수로 해서 다른 곳에서 접근할 수 있도록 함
  85. window.editorInstances = window.editorInstances || new Map();
  86. // CKEditor 초기화
  87. window.initEditor = async function(domID, config = {})
  88. {
  89. const el = document.getElementById(domID);
  90. if (!el || window.editorInstances.has(domID)) {
  91. return;
  92. }
  93. return ClassicEditor
  94. .create(el, {
  95. licenseKey: 'GPL',
  96. extraPlugins: [CustomUploadAdapter, CustomImageAttributesPlugin],
  97. plugins: [
  98. Essentials, Paragraph, Bold, Italic, Underline, Strikethrough, Code,
  99. Subscript, Superscript, RemoveFormat, List, TodoList, Indent, Heading,
  100. Font, Highlight, Alignment, Link, Image, ImageToolbar, ImageCaption,
  101. ImageStyle, ImageResize, ImageUpload, MediaEmbed, CodeBlock, HtmlEmbed,
  102. SpecialCharacters, HorizontalLine, PageBreak, SourceEditing,
  103. FindAndReplace, SelectAll, BlockQuote, Table, TableToolbar, TextPartLanguage
  104. ],
  105. toolbar: {
  106. items: [
  107. 'findAndReplace', 'selectAll', '|',
  108. 'heading', '|',
  109. 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'removeFormat', '|',
  110. 'bulletedList', 'numberedList', 'todoList', '|',
  111. 'outdent', 'indent', '|',
  112. 'undo', 'redo', '|',
  113. 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'highlight', '|',
  114. 'alignment', '|',
  115. 'link', 'insertImage', 'blockQuote', 'insertTable', 'mediaEmbed', 'codeBlock', 'htmlEmbed', '|',
  116. 'specialCharacters', 'horizontalLine', 'pageBreak', '|',
  117. 'textPartLanguage', '|',
  118. 'sourceEditing'
  119. ],
  120. shouldNotGroupWhenFull: true
  121. },
  122. list: {
  123. properties: {
  124. styles: true,
  125. startIndex: true,
  126. reversed: true
  127. }
  128. },
  129. heading: {
  130. options: [
  131. { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
  132. { model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
  133. { model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
  134. { model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
  135. { model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' },
  136. { model: 'heading5', view: 'h5', title: 'Heading 5', class: 'ck-heading_heading5' },
  137. { model: 'heading6', view: 'h6', title: 'Heading 6', class: 'ck-heading_heading6' }
  138. ]
  139. },
  140. placeholder: '내용을 입력해주세요.',
  141. fontFamily: {
  142. options: [
  143. 'default',
  144. 'Arial, Helvetica, sans-serif',
  145. 'Courier New, Courier, monospace',
  146. 'Georgia, serif',
  147. 'Lucida Sans Unicode, Lucida Grande, sans-serif',
  148. 'Tahoma, Geneva, sans-serif',
  149. 'Times New Roman, Times, serif',
  150. 'Trebuchet MS, Helvetica, sans-serif',
  151. 'Verdana, Geneva, sans-serif'
  152. ],
  153. supportAllValues: true
  154. },
  155. fontSize: {
  156. options: [10, 12, 14, 'default', 18, 20, 22],
  157. supportAllValues: true
  158. },
  159. htmlSupport: {
  160. allow: [
  161. {
  162. name: /.*/,
  163. attributes: true,
  164. classes: true,
  165. styles: true
  166. }
  167. ]
  168. },
  169. htmlEmbed: {
  170. showPreviews: true
  171. },
  172. link: {
  173. decorators: {
  174. addTargetToExternalLinks: true,
  175. defaultProtocol: 'https://',
  176. toggleDownloadable: {
  177. mode: 'manual',
  178. label: 'Downloadable',
  179. attributes: {
  180. download: 'file'
  181. }
  182. }
  183. }
  184. },
  185. image: {
  186. toolbar: [
  187. 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|',
  188. 'toggleImageCaption', 'imageTextAlternative'
  189. ]
  190. },
  191. table: {
  192. contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
  193. },
  194. simpleUpload: {
  195. uploadUrl: null
  196. },
  197. ...config
  198. })
  199. .then(editor => {
  200. editor.plugins.get('FileRepository').createUploadAdapter = loader => new CustomUploadAdapter(loader);
  201. // 에디터 저장
  202. window.editorInstances.set(domID, editor);
  203. })
  204. .catch(error => {
  205. console.error('Error initializing CKEditor:', error);
  206. });
  207. };
  208. // 에디터 제거
  209. window.destroyEditor = async function(domID) {
  210. const editor = window.editorInstances.get(domID);
  211. if (editor) {
  212. await editor.destroy();
  213. window.editorInstances.delete(domID);
  214. }
  215. };
  216. document.querySelectorAll('.ck-editor').forEach(e => {
  217. initEditor(e.id);
  218. });
  219. </script>