wysiwygpanel.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. (function() {
  2. /**
  3. * wysiwyg 영역의 컨텐츠를 수정, 관리하기 위한 클래스로,
  4. * 편집 영역에 해당하는 iframe 객체에 접근하여 이벤트를 부여하거나 속성 값들을 읽거나 변경한다.
  5. *
  6. * @class
  7. * @extends Trex.Canvas.BasedPanel
  8. * @param {Object} canvas
  9. * @param {Object} config - canvas의 config
  10. */
  11. Trex.Canvas.WysiwygPanel = Trex.Class.create(/** @lends Trex.Canvas.WysiwygPanel.prototype */{
  12. /** @ignore */
  13. $extend: Trex.Canvas.BasedPanel,
  14. /** @ignore */
  15. $const: {
  16. /** @name Trex.Canvas.WysiwygPanel.__MODE */
  17. __MODE: Trex.Canvas.__WYSIWYG_MODE,
  18. EVENT_BINDING_DELAY: 500
  19. },
  20. initialize: function(canvas, canvasConfig) {
  21. this.$super.initialize(canvas, canvasConfig);
  22. this.canvasConfig = canvasConfig;
  23. this.iframe = this.el;
  24. this.wysiwygWindow = this.iframe.contentWindow;
  25. this.onceWysiwygFocused = false;
  26. var self = this;
  27. var iframeLoader = new Trex.WysiwygIframeLoader(this.iframe, canvasConfig.wysiwygCatalystUrl, canvasConfig.doctype);
  28. iframeLoader.load(function(doc) {
  29. self.wysiwygDoc = doc;
  30. self.initializeSubModules(doc);
  31. installHyperscript(self.wysiwygWindow, self.wysiwygDoc);
  32. self.makeEditable();
  33. self.applyBodyStyles(self.canvasConfig.styles);
  34. self.applyCustomCssText(self.canvasConfig.customCssText);
  35. self.clearContent();
  36. self.bindEvents(canvas);
  37. Editor.__PANEL_LOADED = _TRUE;
  38. $tx.observe(self.wysiwygWindow, 'focus', function onWysiwygFocused() {
  39. if (!self.onceWysiwygFocused) {
  40. self.onceWysiwygFocused = true;
  41. }
  42. });
  43. if ($tx.msie_nonstd) {
  44. var htmlEl = self.wysiwygDoc.getElementsByTagName('html');
  45. if (htmlEl && htmlEl[0]) {
  46. $tx.observe(htmlEl[0], 'click', function (event) {
  47. var target = $tx.element(event);
  48. if (canvas.canHTML() && htmlEl[0] == target) {
  49. self.focus();
  50. }
  51. });
  52. }
  53. }
  54. canvas.fireJobs(Trex.Ev.__IFRAME_LOAD_COMPLETE, doc);
  55. });
  56. },
  57. _bodyHeight : 0,
  58. _bodyContentHeight : 0,
  59. initializeSubModules: function(doc) {
  60. var win = this.wysiwygWindow;
  61. this.processor = new Trex.Canvas.ProcessorP(win, doc);
  62. this.webfontLoader = new Trex.WebfontLoader(doc, this.canvasConfig);
  63. },
  64. /**
  65. * WYSIWYG 영역 iframe을 편집 가능한 상태로 변경한다.
  66. */
  67. makeEditable: function () {
  68. if (this.canvasConfig.readonly) {
  69. return;
  70. }
  71. if (this.wysiwygDoc.body.contentEditable) {
  72. this.wysiwygDoc.body.contentEditable = _TRUE;
  73. } else {
  74. var self = this;
  75. setTimeout(function () {
  76. try {
  77. self.wysiwygDoc.designMode = "On";
  78. if ($tx.gecko) {
  79. self.wysiwygDoc.execCommand("enableInlineTableEditing", _FALSE, _FALSE);
  80. }
  81. } catch (e) {
  82. self.designModeActivated = _FALSE;
  83. }
  84. }, 10);
  85. }
  86. },
  87. /**
  88. * panel의 이름을 리턴한다.
  89. * @function
  90. * @returns {String} 'html'
  91. */
  92. getName: function() {
  93. return this.constructor.__MODE;
  94. },
  95. /**
  96. * wysiwyg 영역의 window 객체를 넘겨준다.
  97. * @function
  98. * @returns {Element} wysiwyg 영역의 window 객체
  99. */
  100. getWindow: function() {
  101. return this.wysiwygWindow;
  102. },
  103. /**
  104. * wysiwyg 영역의 document 객체를 넘겨준다.
  105. * @function
  106. * @returns {Element} wysiwyg 영역의 document 객체
  107. */
  108. getDocument: function() {
  109. return this.wysiwygDoc;
  110. },
  111. /**
  112. * wysiwyg 영역에 쓰여진 컨텐츠를 얻어온다.
  113. * @function
  114. * @returns {String} 컨텐츠 문자열
  115. */
  116. getContent: function() {
  117. return this.wysiwygDoc.body.innerHTML;
  118. },
  119. /**
  120. * wysiwyg 영역의 컨텐츠를 주어진 문자열로 수정한다.
  121. * @function
  122. * @param {String} contentHTML - 컨텐츠
  123. */
  124. setContent: function(contentHTML) {
  125. contentHTML = this.doPreFilter(contentHTML);
  126. this.setBodyHTML(contentHTML);
  127. this.doPostFilter(this.wysiwygDoc.body);
  128. },
  129. doPreFilter: function(contentHTML) {
  130. if (contentHTML) {
  131. contentHTML = removeWordJoiner(contentHTML);
  132. contentHTML = preventRemovingNoScopeElementInIE(contentHTML);
  133. }
  134. return contentHTML;
  135. },
  136. setBodyHTML: function(content) {
  137. this.wysiwygDoc.body.innerHTML = content || $tom.EMPTY_PARAGRAPH_HTML;
  138. },
  139. doPostFilter: function(body) {
  140. makeEmptyParagraphVisibleInIE(body);
  141. },
  142. /**
  143. * 편집 문서 body의 HTML을 모두 지우고 기본 마크업을 세팅한다.
  144. */
  145. clearContent: function() {
  146. this.setContent("");
  147. },
  148. /**
  149. * 현재 wysiwyg 영역의 수직 스크롤 값을 얻어온다.
  150. * @function
  151. * @returns {Number} 수직 스크롤 값
  152. */
  153. getScrollTop: function() {
  154. return $tom.getScrollTop(this.wysiwygDoc);
  155. },
  156. /**
  157. * wysiwyg 영역의 수직 스크롤 값을 셋팅한다.
  158. * @function
  159. * @param {Number} scrollTop - 수직 스크롤 값
  160. */
  161. setScrollTop: function(scrollTop) {
  162. $tom.setScrollTop(this.wysiwygDoc, scrollTop);
  163. },
  164. /**
  165. * 현재 wysiwyg 영역의 수평 스크롤 값을 얻어온다.
  166. * @function
  167. * @returns {Number} 수평 스크롤 값
  168. */
  169. getScrollLeft: function() {
  170. return $tom.getScrollLeft(this.wysiwygDoc);
  171. },
  172. /**
  173. * 생성된 Processor 객체를 리턴한다.
  174. * @function
  175. * @returns {Object} Processor 객체
  176. */
  177. getProcessor: function() {
  178. return this.processor;
  179. },
  180. /**
  181. * 만약 processor 객체가 준비되었다면 processor 객체를 argument로 넘기며 주어진 함수를 수행한다.
  182. * @param fn {function} processor 객체를 argument로 받아 실행할 함수
  183. */
  184. ifProcessorReady: function(fn) {
  185. if (this.processor) {
  186. fn(this.processor);
  187. }
  188. },
  189. /**
  190. * 스타일명으로 wysiwyg 영역의 스타일 값을 얻어온다.
  191. * @function
  192. * @param {String} name - 스타일명
  193. * @returns {String} 해당 스타일 값
  194. */
  195. getStyle: function(name) {
  196. return $tx.getStyle(this.wysiwygDoc.body, name);
  197. },
  198. /**
  199. * wysiwyg 영역에 스타일을 적용한다.
  200. * @function
  201. * @param {Object} styles - 적용할 스타일
  202. */
  203. addStyle: function(styles) {
  204. $tx.setStyleProperty(this.wysiwygDoc.body, styles);
  205. },
  206. /**
  207. * 주어진 문서의 body element 에 CSS 속성을 지정한다. 단 폰트 관련 속성은 제외한다.
  208. * @param doc {HTMLDocument}
  209. * @param styles {Object} key: value 형태의 CSS property 모음
  210. */
  211. setBodyStyle: function(doc, styles) {
  212. var excluded = excludeNotAllowed(styles);
  213. $tx.setStyleProperty(doc.body, excluded);
  214. },
  215. /**
  216. * 주어진 문서에 폰트 관련 CSS 속성을 지정한다
  217. * @param doc {HTMLDocument}
  218. * @param styles {Object} key: value 형태의 CSS property 모음
  219. */
  220. setFontStyle: function(doc, styles) {
  221. var extendedStyles = Object.extend(styles, {
  222. 'browser': $tx.browser,
  223. 'pMarginZero': this.canvasConfig.pMarginZero ? "true" : "false"
  224. });
  225. var cssText = new Template([
  226. "#{if:pMarginZero=='true'}p { margin:0; padding:0; }#{/if:pMarginZero}",
  227. "body, td, button { color:#{color}; font-size:#{fontSize}; font-family:#{fontFamily}; line-height:#{lineHeight}; }",
  228. "a, a:hover, a:link, a:active, a:visited { color:#{color}; }",
  229. "div.txc-search-border { border-color:#{color}; }",
  230. "div.txc-search-opborder { border-color:#{color}; }",
  231. "img.tx-unresizable { width: auto !important; height: auto !important; }",
  232. "button a { text-decoration:none #{if:browser=='firefox'}!important#{/if:browser}; color:#{color} #{if:browser=='firefox'}!important#{/if:browser}; }"
  233. ].join("\n")).evaluate(extendedStyles);
  234. $tx.applyCSSText(doc, cssText);
  235. },
  236. applyBodyStyles: function(styles) {
  237. var doc = this.wysiwygDoc;
  238. try {
  239. this.setFontStyle(doc, styles);
  240. this.setBodyStyle(doc, styles);
  241. } catch(e) {
  242. }
  243. },
  244. applyCustomCssText: function (cssText) {
  245. if (!cssText) {
  246. return;
  247. }
  248. var doc = this.wysiwygDoc;
  249. try {
  250. $tx.applyCSSText(doc, cssText);
  251. } catch (ignore) {}
  252. },
  253. setRule: function (selector, value) {
  254. var styleElem, sheet, rules;
  255. try {
  256. styleElem = this.wysiwygDoc.getElementById("txStyleForSetRule");
  257. sheet = styleElem.sheet ? styleElem.sheet : styleElem.styleSheet;
  258. rules = sheet.cssRules ? sheet.cssRules : sheet.rules;
  259. if (sheet.insertRule) { // all browsers, except IE before version 9
  260. if (0 < rules.length) {
  261. sheet.deleteRule(0);
  262. }
  263. if (selector) {
  264. sheet.insertRule(selector + "{" + value + "}", 0);
  265. }
  266. } else { // Internet Explorer before version 9
  267. if (sheet.addRule) {
  268. if (0 < rules.length) {
  269. sheet.removeRule(0);
  270. }
  271. if (selector) {
  272. sheet.addRule(selector, value, 0);
  273. }
  274. }
  275. }
  276. } catch (ignore) {}
  277. },
  278. /**
  279. * iframe에서 발생하는 각종 event 들을 observing 하기 시작한다.
  280. */
  281. bindEvents: function(canvas) {
  282. var eventBinder = new Trex.WysiwygEventBinder(this.wysiwygWindow, this.wysiwygDoc, canvas, this.processor);
  283. setTimeout(function() {
  284. eventBinder.bindEvents();
  285. }, this.constructor.EVENT_BINDING_DELAY); // why delay 500ms?
  286. },
  287. /**
  288. * panel 엘리먼트를 가지고 온다.
  289. * @function
  290. */
  291. getPanel: function(config) {
  292. var id = config.initializedId || "";
  293. return $must("tx_canvas_wysiwyg" + id, "Trex.Canvas.WysiwygPanel");
  294. },
  295. //#1454
  296. setHeightBody: function(height){
  297. var body = this.wysiwygWindow.document.body;
  298. var marginPaddingTop = parseInt($tx.getStyle(body, 'margin-top')) + parseInt($tx.getStyle(body, 'padding-top'));
  299. var marginPaddingBottom = parseInt($tx.getStyle(body, 'margin-bottom')) + parseInt($tx.getStyle(body, 'padding-bottom'));
  300. height = parseInt(height) - marginPaddingTop - marginPaddingBottom;
  301. body.style.height = height.toPx();
  302. this._bodyHeight = height;
  303. },
  304. setPanelHeight: function(height) {
  305. var self = this;
  306. function timesTry(n){
  307. if(n === 0) return;
  308. try{
  309. self.setHeightBody(height);
  310. }catch(e){
  311. setTimeout(timesTry.bind(this, n-1), 30);
  312. }
  313. }
  314. //초기화 중에 body가 초기화 되지 않았기 때문에 body가 오류날 확률이 있음. 10번 시도 하는 로직 추가
  315. timesTry(10);
  316. self.$super.setPanelHeight(height);
  317. },
  318. /**
  319. * panel 엘리먼트를 감싸고 있는 wrapper 엘리먼트를 가지고 온다.
  320. * @function
  321. */
  322. getHolder: function(config) {
  323. var id = config.initializedId || "";
  324. return $must("tx_canvas_wysiwyg_holder" + id, "Trex.Canvas.WysiwygPanel");
  325. },
  326. /**
  327. * wysiwyg 영역에 포커스를 준다.
  328. * @function
  329. */
  330. focus: function() {
  331. this.ifProcessorReady(function(processor) {
  332. processor.focus();
  333. });
  334. },
  335. ensureFocused: function () {
  336. if (!this.onceWysiwygFocused) {
  337. this.onceWysiwygFocused = true;
  338. this.focus();
  339. }
  340. },
  341. /**
  342. * wysiwyg panel을 보이게한다.
  343. * @function
  344. */
  345. show: function() {
  346. this.$super.show();
  347. this.ifProcessorReady(function(processor) {
  348. setTimeout(function() {
  349. try {
  350. processor.focusOnTop(); //한메일에서 모드 변경시 focus 제일 위로 가게함.. (주의: 현재 다른 서비스는 Bottom 으로 되어있음)
  351. } catch(e) {
  352. }
  353. }, 100);
  354. });
  355. },
  356. /**
  357. * wysiwyg panel을 감춘다.
  358. * @function
  359. */
  360. hide: function() {
  361. this.ifProcessorReady(function(processor) {
  362. processor.blur();
  363. });
  364. this.$super.hide();
  365. },
  366. /**
  367. * 컨텐츠를 파싱하여 사용되고 있는 웹폰트가 있으면, 웹폰트 css를 로딩한다.<br/>
  368. * 로딩속도를 향상시키기 위해 본문을 파싱하여 웹폰트를 사용할 경우에만 동적으로 웹폰트 CSS를 호출한다.
  369. * @function
  370. * @param {String} content - 컨텐츠
  371. */
  372. includeWebfontCss: function(content) {
  373. this.webfontLoader.load(content);
  374. },
  375. /**
  376. * 본문에 사용된 웹폰트명 목록을 리턴한다.
  377. * @function
  378. * @returns {Array} 사용하고 있는 웹폰트명 목록
  379. */
  380. getUsedWebfont: function() {
  381. return this.webfontLoader.getUsed();
  382. },
  383. /**
  384. * 특정 노드의 wysiwyg 영역에서의 상대 위치를 얻어온다.
  385. * @function
  386. * @param {Element} node - 특정 노드
  387. * @returns {Object} position 객체 { x: number, y: number, width: number, height: number }
  388. */
  389. getPositionByNode: function(node) {
  390. var wysiwygRelative = new Trex.WysiwygRelative(this.iframe);
  391. return wysiwygRelative.getRelative(node);
  392. }
  393. });
  394. function excludeNotAllowed(style) {
  395. var notAllowed = ["color", "fontSize", "fontFamily", "lineHeight"];
  396. var excluded = Object.clone(style);
  397. for (var i = 0; i < notAllowed.length; i++) {
  398. delete excluded[notAllowed[i]];
  399. }
  400. return excluded;
  401. }
  402. function removeWordJoiner(content) {
  403. return content.replace(Trex.__WORD_JOINER_REGEXP, "");
  404. }
  405. /*
  406. * NOTE: FTDUEDTR-900
  407. */
  408. function preventRemovingNoScopeElementInIE(markup) {
  409. if ($tx.msie) {
  410. markup = markup.replace(/(<script|<style)/i, Trex.__WORD_JOINER + "$1");
  411. }
  412. return markup;
  413. }
  414. /*
  415. * IE에서 빈 p 엘리먼트는 높이를 갖지 않고 화면에 표시되지 않는다. 따라 빈 문단 표시를 위하여 <P>&nbsp;</P> 를
  416. * 사용하는 데, 편집시에는 &nbsp;가 빈칸 한칸으로서 자리를 차지하여 걸리적 거리므로 이를 제거하여 준다.
  417. * 이와 같이 contentEditable 환경에서 &nbsp; 를 주었다가 빼면 빈 엘리먼트임에도 불구하고 문단으로서 높이를 유지한다.
  418. *
  419. * 의문 1.
  420. * 그런데 LI는 왜 하는 걸까 -_-^
  421. * 의문 2.
  422. * <P></P> 를 하나의 문단으로 여기고 높이를 잡아주는 게 맞을까? 글 조회시에는 표시 되지 않을텐데...
  423. * 편집시와 조회시 불일치 발생한다.
  424. * 참고 이슈 #FTDUEDTR-1121
  425. * 의문 3.
  426. * 저장시 빈 문단에 &nbsp;를 다시 넣어주는 듯 한데, 어디서 해주는 걸까
  427. * @param body
  428. */
  429. function makeEmptyParagraphVisibleInIE(body) {
  430. if ($tx.msie_nonstd) {
  431. var pNodes = $tom.collectAll(body, 'p,li');
  432. for (var i = 0, len = pNodes.length; i < len; i++) {
  433. var node = pNodes[i];
  434. if ($tom.getLength(node) === 0 && node.tagName.toLowerCase() !== 'p') { //#FTDUEDTR-1121
  435. try {
  436. node.innerHTML = '&nbsp;';
  437. } catch(ignore) {
  438. }
  439. }
  440. if ($tom.getLength(node) === 1 && node.innerHTML === '&nbsp;') {
  441. node.innerHTML = '';
  442. }
  443. }
  444. }
  445. }
  446. })();
  447. Trex.module("canvas set focus on mousedown event. only IE.",
  448. function(editor, toolbar, sidebar, canvas, config) {
  449. if (!$tx.msie_std) {
  450. return;
  451. }
  452. canvas.observeJob(Trex.Ev.__CANVAS_PANEL_MOUSEUP, function(ev){
  453. if ($tx.isLeftClick(ev)) {
  454. var tagName = $tx.element(ev).tagName;
  455. if (tagName.toLocaleLowerCase() == 'html') {
  456. canvas.focusOnBottom();
  457. }
  458. }
  459. });
  460. });
  461. Trex.module("auto body resize",
  462. function(editor, toolbar, sidebar, canvas, config){
  463. canvas.observeJob(Trex.Ev.__IFRAME_LOAD_COMPLETE, function() {
  464. var beforeHTML = '';
  465. var bodyHeight = 0;
  466. var _panel = canvas.getPanel('html');
  467. bodyHeight = _panel._bodyHeight;
  468. canvas.observeJob('canvas.height.change', function(h){
  469. bodyHeight = parseInt(_panel._bodyHeight);
  470. });
  471. function _resize(){
  472. if(!canvas.isWYSIWYG()) return;
  473. var _doc = _panel.getDocument();
  474. var _body = _doc.body;
  475. var _html = _body.innerHTML;
  476. if(beforeHTML === _html) return;
  477. beforeHTML = _html;
  478. _body.style.height = '';
  479. var _h = $tx.getDimensions(_body).height;
  480. _panel._bodyContentHeight = _h;
  481. if(_h<bodyHeight) {
  482. _body.style.height = bodyHeight.toPx();
  483. }
  484. }
  485. setInterval(_resize, 200);
  486. });
  487. }
  488. );