config.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // jQuery not really required, it's here to overcome an inability to pass configuration options to the fiddle remotely
  2. // Custom example logic
  3. /**
  4. * TinyMCE Load
  5. */
  6. if (typeof (tinyMCE) == "undefined") {
  7. console.log('Requires TinyMCE.');
  8. }
  9. let isMobile = !tinymce.Env.desktop; // (tinymce.Env.phone || tinymce.Env.tablet || false);
  10. let pluploadHandler,
  11. menubar,
  12. plugins,
  13. toolbar,
  14. cssText;
  15. cssText = "div.emoticon-group {margin:0; padding:0;}";
  16. cssText += "div.emoticon-group > A.emoticon-group-item {margin:0;padding:0 9px 0 11px; float:left;width:60px;height:60px; list-style:none;cursor:pointer;}";
  17. cssText += "div.emoticon-group > A.emoticon-group-item > IMG.emoticon-group-item-icon {max-width:100%;max-height:100%;}";
  18. cssText += "div.mce-path > .mce-path-item {display:none !important;}";
  19. cssText += "@media all and (max-width: 480px;) {";
  20. cssText += "ul.emoticon-group > li.emoticon-group-item {width:32px;}";
  21. cssText += "}";
  22. if(isMobile) {
  23. cssText += "@media all and (max-width: 480px;){";
  24. cssText += ".tinycme-full .mce-edit-area {display:flex;flex-flow:column;}";
  25. cssText += ".tinycme-full .mce-edit-area iframe {flex:1 1 auto;}";
  26. cssText += ".tinycme-full {height:100%;}";
  27. cssText += ".tinycme-full .mce-tinymce.mce-container {width:100%;height:100%;/*border:0;*/}";
  28. cssText += ".tinycme-full .mce-panel{/*border:0;*/}";
  29. cssText += ".tinycme-full .mce-container-body.mce-stack-layout {display: flex; flex-flow: column;height: 100%;}";
  30. cssText += ".tinycme-full .mce-stack-layout-item{ flex: 0 0 auto;}";
  31. cssText += ".tinycme-full .mce-edit-area{flex:1 1 auto;}";
  32. cssText += ".tinycme-full .mce-window .mce-btn.mce-colorbutton {display:none}";
  33. cssText += ".tinycme-full .mce-window-head .mce-dragh {cursor:default !important;} }";
  34. }
  35. // Viewport Add HTML5기반 (IE 10, Chrome 9, Opera 12.1, Safari 5.1, Firefox 6.0
  36. if (!$('head > meta[name="viewport"]:last').length) {
  37. $('<meta name="viewport" content="width=device-width, initial-scale=1.0" />').append('head:last');
  38. }
  39. $('<style>').prop("type", "text/css").attr({'id': 'tinymce-css-responsive'}).html(cssText).appendTo('head:last');
  40. $.ajaxSetup({'async': false});
  41. // 허용 iframe 주소 기본값
  42. if(typeof whiteIframe == "undefined" || whiteIframe == null || whiteIframe == "") {
  43. var whiteIframe = ['www.youtube.com', 'www.youtube-nocookie.com', 'maps.google.co.kr',
  44. 'maps.google.com', 'flvs.daum.net', 'player.vimeo.com', 'sbsplayer.sbs.co.kr',
  45. 'serviceapi.rmcnmv.naver.com', 'serviceapi.nmv.naver.com', 'www.mgoon.com',
  46. 'videofarm.daum.net', 'player.sbs.co.kr', 'sbsplayer.sbs.co.kr', 'www.tagstory.com',
  47. 'play.tagstory.com', 'flvr.pandora.tv', 'youtu.be', 'vimeo.com', 'dailymotion.com', 'tvpot.daum.net'];
  48. }
  49. // 이모티콘 사용여부
  50. if(typeof useEmoticon == "undefined" || useEmoticon == null || useEmoticon === "") {
  51. var useEmoticon = 'N';
  52. }
  53. // 관리자와 사용자 구분하여 기능 구분
  54. if(window.location.href.indexOf('admin') <= -1) // 사용자
  55. {
  56. // autoresize라는 플러그인도 있음.
  57. menubar = false;
  58. plugins = 'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker searchreplace visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table contextmenu directionality emoticons template paste textcolor codesample';
  59. toolbar = 'fontselect | fontStyle | fontsizeselect | bold underline strikethrough forecolor backcolor alignleft aligncenter alignright alignjustify | link imageUpload media table | code | codesample';
  60. } else { // 관리자
  61. // wordcount 제외시킴
  62. menubar = true;
  63. plugins = 'print preview searchreplace autolink directionality visualblocks visualchars fullscreen link media template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists textcolor imagetools contextmenu colorpicker textpattern help code codesample';
  64. toolbar = 'formatselect | bold italic strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | numlist bullist outdent indent | removeformat | link imageUpload media | code | codesample';
  65. }
  66. // Placeholder 기능 추가
  67. let label = {
  68. init: function(e) {
  69. let placeholderText = (e.getElement().getAttribute("placeholoer") || e.settings.placeholder);
  70. let placeholderAttrs = (e.settings.placeholderAttrs || {
  71. style: {
  72. position: 'absolute',
  73. top: '2px',
  74. left: 0,
  75. color: '#959595',
  76. padding: '.25%',
  77. margin: '5px',
  78. width: 'auto',
  79. 'font-size': '17px !important;',
  80. overflow: 'hidden',
  81. 'line-height': '24px',
  82. 'white-space': 'pre-wrap'
  83. }
  84. });
  85. let contentAreaContainer = e.getContentAreaContainer();
  86. tinymce.DOM.setStyle(contentAreaContainer, 'position', 'relative');
  87. this.el = tinymce.DOM.add(contentAreaContainer, "label", placeholderAttrs, placeholderText);
  88. },
  89. focus: function(e) {
  90. if (!e.settings.readonly === true) {
  91. this.hide();
  92. }
  93. e.execCommand('mceFocus', false);
  94. },
  95. blur: function(e) {
  96. if (e.getContent() == '') {
  97. this.show();
  98. } else {
  99. this.hide();
  100. }
  101. },
  102. hide: function() {
  103. tinymce.DOM.setStyle(this.el, 'display', 'none');
  104. },
  105. show: function() {
  106. tinymce.DOM.setStyle(this.el, 'display', '');
  107. }
  108. };
  109. let setting = {
  110. selector: '.tinymce-editor',
  111. language: 'ko_KR',
  112. menubar: menubar,
  113. theme: 'modern',
  114. skin: 'default',
  115. plugins: plugins,
  116. width: 'auto',
  117. inline: false, // <br data-mce-bogus="1"> 삭제
  118. forced_root_block: "", // p tag 비 활성화
  119. min_height: 400, // 에디터 세로크기
  120. // autoresize_min_height: 400, // 에디터 최소 세로크기
  121. // autoresize_bottom_margin: 0,
  122. // autoresize_on_init: true,
  123. content_style:
  124. "body {font-family: Nanum-Square-Neo-Regular;}" +
  125. // 표 열/행 경계 드래그 바. default 스킨의 content.min.css 에는 이 규칙이 빠져 있어(lightgray 에만 존재)
  126. // 경계선에 마우스를 올려도 커서가 바뀌지 않아 드래그로 너비 조절이 가능한지 알 수 없다.
  127. ".ephox-snooker-resizer-bar {background-color: #2276d2; opacity: 0; user-select: none;}" +
  128. ".ephox-snooker-resizer-bar:hover {opacity: 0.35;}" +
  129. ".ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging {opacity: 0.2;}" +
  130. ".ephox-snooker-resizer-cols {cursor: col-resize;}" +
  131. ".ephox-snooker-resizer-rows {cursor: row-resize;}",
  132. font_formats: "Default='나눔스퀘어'" +
  133. ";나눔스퀘어=Nanum-Square-Neo-Regular, 나눔스퀘어" +
  134. ";돋움=dotum, 돋움" +
  135. ";굴림=gulim, 굴림" +
  136. ";바탕=batang, 바탕" +
  137. ";궁서=gungsuh, 궁서" +
  138. ";맑은 고딕=malgun gothic, 맑은 고딕" +
  139. ";Arial=arial,helvetica, verdana, sans-serif" +
  140. ";Andale Mono=andale mono, monospace" +
  141. ";Tahoma=tahoma,arial, helvetica, sans-serif" +
  142. ";Verdana=verdana, geneva, sans-serif",
  143. codesample_languages: [
  144. {text: "HTML/XML", value: "markup"},
  145. {text: "JavaScript", value: "javascript"},
  146. {text: "CSS", value: "css"},
  147. {text: "PHP", value: "php"},
  148. {text: "Ruby", value: "ruby"},
  149. {text: "Python", value: "python"},
  150. {text: "Java", value: "java"},
  151. {text: "C", value: "c"},
  152. {text: "C#", value: "csharp"},
  153. {text: "C++", value: "cpp"},
  154. {text: "scss", value: "scss"},
  155. {text: "SQL", value: "sql"},
  156. {text: "YAML", value: "yaml"},
  157. {text: "nginx", value: "nginx"},
  158. {text: "JSON", value: "json"},
  159. {text: "go", value: "go"},
  160. {text: "Bash", value: "bash"},
  161. {text: "Shell", value: "shell-session"}
  162. ],
  163. branding: false, // 아래 `POWERED BY TINYMCE` 표시 삭제
  164. toolbar: toolbar,
  165. fontsize_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt',
  166. statusbar: true, // 하단 단어 개수, p 태그 지정 상태창 숨김
  167. directionality: 'ltr',
  168. visualblocks_default_state: false, // 선택 영역 지정
  169. // 링크 설정
  170. link_title: false,
  171. target_list: false,
  172. default_link_target: "_blank",
  173. // 이미지 설정
  174. image_dimensions: false, // 이미지 크기
  175. file_browser_url: "/editor/uploader", // 라우터에 의한 이동
  176. image_advtab: true,
  177. paste_data_images: true,
  178. paste_postprocess: (editor, args) => {
  179. let images = args.node.getElementsByTagName("img");
  180. if(images.length > 0) {
  181. for(let img of images) {
  182. img.dataset.target = "thumb";
  183. }
  184. }
  185. },
  186. // 이미지 크기 조정
  187. elementpath: false,
  188. relative_urls: true,
  189. remove_script_host: true,
  190. // 표 설정
  191. // 새 표는 table_default_styles 기본값(width:100%)으로 삽입되므로, 열 너비는 % 단위 인라인 style 로 기록된다.
  192. object_resizing: true, // 표/이미지 크기 조절 핸들 (false 로 두면 표 열 너비 조절도 함께 꺼진다)
  193. table_resize_bars: true, // 표 경계선 드래그로 열/행 크기 조절
  194. table_toolbar: 'tableprops tablecellprops tabledelete' +
  195. ' | tableinsertrowbefore tableinsertrowafter tabledeleterow' +
  196. ' | tableinsertcolbefore tableinsertcolafter tabledeletecol', // 셀 속성(너비 직접 입력) 추가
  197. // 비디오 설정
  198. media_poster: false, // 포스터 파일
  199. media_alt_source: false, // 대체 소스
  200. media_dimensions: false,
  201. media_filter_html: true,
  202. media_allow_host: whiteIframe
  203. , 'media_url_patterns' : [
  204. {regex : /videofarm\.daum\.net\/controller\/video\/viewer\/Video\.html\?vid=([^&]+)/, type: 'iframe', w: 640, h: 360, url: '//videofarm.daum.net/controller/video/viewer/Video.html?vid=$1&play_loc=undefined&alert=true', allowFullscreen: true}
  205. ]
  206. , 'media_pattern_types' : [
  207. {'regex' : /^(http[s]?:)?\/\/videofarm\.daum\.net/, 'type' : 'iframe' , 'allowFullscreen' : true, 'width': 640, 'height': 360}
  208. ],
  209. extended_valid_elements : "iframe[src|title|width|height|allowfullscreen|frameborder]",
  210. init_instance_callback: function (editor) {
  211. label.init(editor);
  212. label.blur(editor);
  213. tinymce.DOM.bind(label.el, 'click', label.focus(editor));
  214. },
  215. setup: function (editor) {
  216. editor.on('submit', function (e) {
  217. console.log("Editor submited !!");
  218. });
  219. editor.addButton('imageUpload', {
  220. type: 'button',
  221. title: 'Insert/edit image',
  222. icon: 'image',
  223. id: 'imageUpload',
  224. stateSelector: 'img:not([data-mce-object],[data-mce-placeholder],[data-smileys]),figure.image',
  225. onclick: function () {
  226. let dialog = editor.windowManager.open({
  227. title: "사진 첨부하기",
  228. width: 800,
  229. height: 415,
  230. scrollbars: false,
  231. labelby: "imageUpload",
  232. resizable: true,
  233. file: editor.settings.file_browser_url,
  234. close_previous: true,
  235. maximizable: true,
  236. inline: true,
  237. buttons: [{
  238. text: "확인",
  239. subtype: "primary",
  240. onclick: function (e) {
  241. // pluploadHandler.startUploader(this);
  242. pluploadHandler.insertContent();
  243. }
  244. }, {
  245. text: "닫기",
  246. onclick: function (e) {
  247. pluploadHandler.stopUploader();
  248. }
  249. }]
  250. });
  251. pluploadSetting(dialog);
  252. }
  253. });
  254. // 초기 설정
  255. editor.on('init', function(e) {
  256. if(useEmoticon === 'Y') {
  257. let div = $('<div class="mce-container mce-panel mce-stack-layout-item">');
  258. $(e.getContentAreaContainer()).after(div); // 에디터 하단에 새로운 영역 생성
  259. setEmoticons(e, div);
  260. selfAutoResize();
  261. (typeof (selfAutoResize) == 'function' && e.on("nodechange setcontent keyup FullscreenStateChanged", selfAutoResize));
  262. console.log("editor emoticon loaded");
  263. }
  264. });
  265. // 팝업 창 오픈
  266. editor.on("OpenWindow", function(arg) {
  267. let hWin = arg.win;
  268. hWin.settings.maxWidth = (hWin.settings.maxWidth || hWin.settings.width || hWin._layoutRect.w);
  269. hWin.settings.maxHeight = (hWin.settings.maxHeight || hWin.settings.height || hWin._layoutRect.h);
  270. $(window).off('resize', windowDialogResize).on('resize', windowDialogResize);
  271. setTimeout(windowDialogResize, 100);
  272. });
  273. // 팝업창 닫기
  274. editor.on("CloseWindow", function() {
  275. $(".wrap-loading").hide();
  276. $(window).off('resize', windowDialogResize);
  277. });
  278. // 에디터 자동저장
  279. editor.on('change', function(e) {
  280. e.target.save();
  281. label.blur(editor);
  282. });
  283. editor.on('focus', function(e) {
  284. label.focus(editor);
  285. });
  286. editor.on('blur', function(e) {
  287. label.blur(editor);
  288. });
  289. editor.on('setContent', function(e) {
  290. // iframe에 입력되는 주소는 hidden으로 저장한다.
  291. let iframeRows = e.target.contentDocument.body.getElementsByTagName("iframe");
  292. if(iframeRows > 0) {
  293. $(e.content).filter("iframe").attr("src", ""); // iframe src 주소
  294. }
  295. label.blur(editor);
  296. });
  297. }
  298. };
  299. tinymce.init(setting);
  300. // 파일 업로드 환경설정
  301. function pluploadSetting(dialog)
  302. {
  303. let iframe = $(dialog.$el).find(".mce-window-body iframe").get(0);
  304. $(iframe).on("load", function () {
  305. let window = this.contentWindow || this;
  306. let document = this.contentDocument || win.document;
  307. pluploadHandler = window;
  308. // 전체삭제 버튼 클릭
  309. $(document).on("click", "div#remove-all", function () {
  310. pluploadHandler.removeThumb();
  311. });
  312. // 개별 삭제 버튼 클릭
  313. $(document).on("click", ".picker-image-remove", function () {
  314. pluploadHandler.removeThumb($(this));
  315. });
  316. // 제대로 로딩이 안되면 팝업 닫기
  317. if(document.firstElementChild.querySelectorAll(".picker-container").length <= 0) {
  318. parent.tinyMCE.activeEditor.windowManager.close(window);
  319. }
  320. console.log('Upload View Loaded.');
  321. });
  322. }
  323. // 에디터 팝업창 크기 자동조절
  324. function windowDialogResize()
  325. {
  326. let editor = tinyMCE.activeEditor;
  327. let hWin = editor.windowManager.getWindows()[0];
  328. if(typeof hWin == 'undefined') {
  329. return;
  330. }
  331. let dialog = $(hWin.$el);
  332. let currentWidth = dialog.width();
  333. let innerWidth = $(window).innerWidth();
  334. let innerHeight = $(window).innerHeight();
  335. let maxWidth = hWin.settings.maxWidth;
  336. let maxHeight = hWin.settings.maxHeight;
  337. let resizeWidth = Math.max(Math.min(innerWidth, maxWidth), 240);
  338. let resizeHeight = Math.max(Math.min(innerHeight, maxHeight), 200);
  339. hWin.resizeTo(resizeWidth, resizeHeight);
  340. let posX = Math.floor((innerWidth / 2) - (dialog.width() / 2)) - 1;
  341. let posY = Math.floor((innerHeight / 2) - (dialog.height() / 2));
  342. if(isMobile) {
  343. posY = innerHeight - (dialog.height() + 2);
  344. }
  345. hWin.moveTo(posX, posY);
  346. setTimeout(function () {
  347. var dw = (currentWidth - 40) - (dialog.width() - 40);
  348. dialog.find('.' + hWin.bodyClasses.prefix + 'textbox').each(function () {
  349. var el = $(this);
  350. el.width(el.width() - dw);
  351. });
  352. }, 0);
  353. }
  354. // 에디터 알림 메시지
  355. function message(message, severity, args)
  356. {
  357. let setting = $.extend({'timeout': 3000, 'closeButton': true, 'icon': (severity || 'notice').replace('error', 'notice')}, args, {'text': message, 'type': severity || 'info'});
  358. tinymce.activeEditor.notificationManager.open(setting);
  359. }
  360. // 팝업 알림 메시지
  361. function popMessage(message)
  362. {
  363. tinymce.activeEditor.windowManager.alert(message);
  364. }
  365. let selfResizeObj;
  366. let selfIframe;
  367. let wrapper = $("div#wrapper");
  368. let oldHeight = wrapper.height();
  369. // 크기 자동조정 기능정의
  370. function resizeDone(wait)
  371. {
  372. clearTimeout(selfResizeObj);
  373. selfResizeObj = null;
  374. if (!selfIframe) {
  375. $(parent.document).find("iframe").each(function () {
  376. return ((this.contentWindow == window) ? (selfIframe = $(this), false) : void 0);
  377. });
  378. if (!selfIframe) {
  379. return true;
  380. }
  381. }
  382. try {
  383. let curHeight;
  384. (curHeight = wrapper.height()) != oldHeight && (selfIframe.height(curHeight), oldHeight = curHeight);
  385. } catch (err) {
  386. alert(err);
  387. }
  388. if (wait > 0) {
  389. selfResizeObj = setTimeout(function () {
  390. resizeDone(--wait);
  391. }, 10);
  392. }
  393. }
  394. // 현재 에디터 크기 자동조정
  395. function selfAutoResize(wait)
  396. {
  397. if(!window.parent) {
  398. return true;
  399. }
  400. // tinymce.activeEditor.execCommand('mceAutoResize', false);
  401. wait = (wait || 1);
  402. selfResizeObj = setTimeout(function() {
  403. resizeDone(--wait);
  404. }, 10);
  405. }
  406. // 이모티콘 설정
  407. function setEmoticons(editor, container)
  408. {
  409. // 현재 창이 관리자 모드 라면 취소
  410. let adminUrl = document.location.href;
  411. if(adminUrl.indexOf("admin") != -1) {
  412. return false;
  413. }
  414. // 이모티콘 로드
  415. let emoticons= new Array();
  416. $.get(tinyMCE.baseURL + '/emoticon.xml', function(data) {
  417. if(typeof data == 'string'){
  418. data = $.parseXML(data);
  419. }
  420. emoticons = $.map(data.documentElement.getElementsByTagName('emoticon'), function(emoticon) {
  421. let s = {};
  422. $.each(emoticon.attributes, function() {
  423. if($.trim(this.nodeValue))
  424. s[this.nodeName] = this.nodeValue;
  425. });
  426. return s;
  427. });
  428. });
  429. container.css({'background-color': '#FFFFFF', 'border-width': '1px 0 0 0'});
  430. let group = $('<DIV />').addClass('emoticon-group').appendTo(container);
  431. $.each(emoticons, function(index) {
  432. $('<img />').attr(this).addClass('emoticon-group-item-icon').appendTo($('<a href="#" onclick="return false;" class="emoticon-group-item"></a>').appendTo(group)).on('click', function(event) {
  433. event.preventDefault();
  434. editor.insertContent('<img src="' + $(this).attr('src') + '" align="middle" data-mce-resize="false" alt="이모티콘_' + index + ' "/>');
  435. });
  436. });
  437. group.append($('<div />').css({'display': 'block', 'width': '100%', 'height': '1px', 'clear': 'both', 'float': 'none'}));
  438. }
  439. // 업로드 이미지 번호 저장
  440. function setImageID(imageID) {
  441. let editor = tinyMCE.activeEditor;
  442. if (typeof imageID == "undefined" || imageID == null) {
  443. message("해당 사진을 올릴 수 없습니다.");
  444. return false;
  445. } else {
  446. let div = $('<input type="hidden" name="imageID" value="' + imageID + '"/>');
  447. $(editor.getContentAreaContainer()).after(div); // 에디터 하단에 새로운 영역 생성
  448. }
  449. }