plugin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. /**
  2. * plugin.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /*global tinymce:true */
  11. tinymce.PluginManager.add('fullpage', function(editor) {
  12. var each = tinymce.each, Node = tinymce.html.Node;
  13. var head, foot;
  14. function showDialog() {
  15. var data = htmlToData();
  16. editor.windowManager.open({
  17. title: 'Document properties',
  18. data: data,
  19. defaults: {type: 'textbox', size: 40},
  20. body: [
  21. {name: 'title', label: 'Title'},
  22. {name: 'keywords', label: 'Keywords'},
  23. {name: 'description', label: 'Description'},
  24. {name: 'robots', label: 'Robots'},
  25. {name: 'author', label: 'Author'},
  26. {name: 'docencoding', label: 'Encoding'}
  27. ],
  28. onSubmit: function(e) {
  29. dataToHtml(tinymce.extend(data, e.data));
  30. }
  31. });
  32. }
  33. function htmlToData() {
  34. var headerFragment = parseHeader(), data = {}, elm, matches;
  35. function getAttr(elm, name) {
  36. var value = elm.attr(name);
  37. return value || '';
  38. }
  39. // Default some values
  40. data.fontface = editor.getParam("fullpage_default_fontface", "");
  41. data.fontsize = editor.getParam("fullpage_default_fontsize", "");
  42. // Parse XML PI
  43. elm = headerFragment.firstChild;
  44. if (elm.type == 7) {
  45. data.xml_pi = true;
  46. matches = /encoding="([^"]+)"/.exec(elm.value);
  47. if (matches) {
  48. data.docencoding = matches[1];
  49. }
  50. }
  51. // Parse doctype
  52. elm = headerFragment.getAll('#doctype')[0];
  53. if (elm) {
  54. data.doctype = '<!DOCTYPE' + elm.value + ">";
  55. }
  56. // Parse title element
  57. elm = headerFragment.getAll('title')[0];
  58. if (elm && elm.firstChild) {
  59. data.title = elm.firstChild.value;
  60. }
  61. // Parse meta elements
  62. each(headerFragment.getAll('meta'), function(meta) {
  63. var name = meta.attr('name'), httpEquiv = meta.attr('http-equiv'), matches;
  64. if (name) {
  65. data[name.toLowerCase()] = meta.attr('content');
  66. } else if (httpEquiv == "Content-Type") {
  67. matches = /charset\s*=\s*(.*)\s*/gi.exec(meta.attr('content'));
  68. if (matches) {
  69. data.docencoding = matches[1];
  70. }
  71. }
  72. });
  73. // Parse html attribs
  74. elm = headerFragment.getAll('html')[0];
  75. if (elm) {
  76. data.langcode = getAttr(elm, 'lang') || getAttr(elm, 'xml:lang');
  77. }
  78. // Parse stylesheets
  79. data.stylesheets = [];
  80. tinymce.each(headerFragment.getAll('link'), function(link) {
  81. if (link.attr('rel') == 'stylesheet') {
  82. data.stylesheets.push(link.attr('href'));
  83. }
  84. });
  85. // Parse body parts
  86. elm = headerFragment.getAll('body')[0];
  87. if (elm) {
  88. data.langdir = getAttr(elm, 'dir');
  89. data.style = getAttr(elm, 'style');
  90. data.visited_color = getAttr(elm, 'vlink');
  91. data.link_color = getAttr(elm, 'link');
  92. data.active_color = getAttr(elm, 'alink');
  93. }
  94. return data;
  95. }
  96. function dataToHtml(data) {
  97. var headerFragment, headElement, html, elm, value, dom = editor.dom;
  98. function setAttr(elm, name, value) {
  99. elm.attr(name, value ? value : undefined);
  100. }
  101. function addHeadNode(node) {
  102. if (headElement.firstChild) {
  103. headElement.insert(node, headElement.firstChild);
  104. } else {
  105. headElement.append(node);
  106. }
  107. }
  108. headerFragment = parseHeader();
  109. headElement = headerFragment.getAll('head')[0];
  110. if (!headElement) {
  111. elm = headerFragment.getAll('html')[0];
  112. headElement = new Node('head', 1);
  113. if (elm.firstChild) {
  114. elm.insert(headElement, elm.firstChild, true);
  115. } else {
  116. elm.append(headElement);
  117. }
  118. }
  119. // Add/update/remove XML-PI
  120. elm = headerFragment.firstChild;
  121. if (data.xml_pi) {
  122. value = 'version="1.0"';
  123. if (data.docencoding) {
  124. value += ' encoding="' + data.docencoding + '"';
  125. }
  126. if (elm.type != 7) {
  127. elm = new Node('xml', 7);
  128. headerFragment.insert(elm, headerFragment.firstChild, true);
  129. }
  130. elm.value = value;
  131. } else if (elm && elm.type == 7) {
  132. elm.remove();
  133. }
  134. // Add/update/remove doctype
  135. elm = headerFragment.getAll('#doctype')[0];
  136. if (data.doctype) {
  137. if (!elm) {
  138. elm = new Node('#doctype', 10);
  139. if (data.xml_pi) {
  140. headerFragment.insert(elm, headerFragment.firstChild);
  141. } else {
  142. addHeadNode(elm);
  143. }
  144. }
  145. elm.value = data.doctype.substring(9, data.doctype.length - 1);
  146. } else if (elm) {
  147. elm.remove();
  148. }
  149. // Add meta encoding
  150. elm = null;
  151. each(headerFragment.getAll('meta'), function(meta) {
  152. if (meta.attr('http-equiv') == 'Content-Type') {
  153. elm = meta;
  154. }
  155. });
  156. if (data.docencoding) {
  157. if (!elm) {
  158. elm = new Node('meta', 1);
  159. elm.attr('http-equiv', 'Content-Type');
  160. elm.shortEnded = true;
  161. addHeadNode(elm);
  162. }
  163. elm.attr('content', 'text/html; charset=' + data.docencoding);
  164. } else {
  165. elm.remove();
  166. }
  167. // Add/update/remove title
  168. elm = headerFragment.getAll('title')[0];
  169. if (data.title) {
  170. if (!elm) {
  171. elm = new Node('title', 1);
  172. addHeadNode(elm);
  173. } else {
  174. elm.empty();
  175. }
  176. elm.append(new Node('#text', 3)).value = data.title;
  177. } else if (elm) {
  178. elm.remove();
  179. }
  180. // Add/update/remove meta
  181. each('keywords,description,author,copyright,robots'.split(','), function(name) {
  182. var nodes = headerFragment.getAll('meta'), i, meta, value = data[name];
  183. for (i = 0; i < nodes.length; i++) {
  184. meta = nodes[i];
  185. if (meta.attr('name') == name) {
  186. if (value) {
  187. meta.attr('content', value);
  188. } else {
  189. meta.remove();
  190. }
  191. return;
  192. }
  193. }
  194. if (value) {
  195. elm = new Node('meta', 1);
  196. elm.attr('name', name);
  197. elm.attr('content', value);
  198. elm.shortEnded = true;
  199. addHeadNode(elm);
  200. }
  201. });
  202. var currentStyleSheetsMap = {};
  203. tinymce.each(headerFragment.getAll('link'), function(stylesheet) {
  204. if (stylesheet.attr('rel') == 'stylesheet') {
  205. currentStyleSheetsMap[stylesheet.attr('href')] = stylesheet;
  206. }
  207. });
  208. // Add new
  209. tinymce.each(data.stylesheets, function(stylesheet) {
  210. if (!currentStyleSheetsMap[stylesheet]) {
  211. elm = new Node('link', 1);
  212. elm.attr({
  213. rel: 'stylesheet',
  214. text: 'text/css',
  215. href: stylesheet
  216. });
  217. elm.shortEnded = true;
  218. addHeadNode(elm);
  219. }
  220. delete currentStyleSheetsMap[stylesheet];
  221. });
  222. // Delete old
  223. tinymce.each(currentStyleSheetsMap, function(stylesheet) {
  224. stylesheet.remove();
  225. });
  226. // Update body attributes
  227. elm = headerFragment.getAll('body')[0];
  228. if (elm) {
  229. setAttr(elm, 'dir', data.langdir);
  230. setAttr(elm, 'style', data.style);
  231. setAttr(elm, 'vlink', data.visited_color);
  232. setAttr(elm, 'link', data.link_color);
  233. setAttr(elm, 'alink', data.active_color);
  234. // Update iframe body as well
  235. dom.setAttribs(editor.getBody(), {
  236. style : data.style,
  237. dir : data.dir,
  238. vLink : data.visited_color,
  239. link : data.link_color,
  240. aLink : data.active_color
  241. });
  242. }
  243. // Set html attributes
  244. elm = headerFragment.getAll('html')[0];
  245. if (elm) {
  246. setAttr(elm, 'lang', data.langcode);
  247. setAttr(elm, 'xml:lang', data.langcode);
  248. }
  249. // No need for a head element
  250. if (!headElement.firstChild) {
  251. headElement.remove();
  252. }
  253. // Serialize header fragment and crop away body part
  254. html = new tinymce.html.Serializer({
  255. validate: false,
  256. indent: true,
  257. apply_source_formatting : true,
  258. indent_before: 'head,html,body,meta,title,script,link,style',
  259. indent_after: 'head,html,body,meta,title,script,link,style'
  260. }).serialize(headerFragment);
  261. head = html.substring(0, html.indexOf('</body>'));
  262. }
  263. function parseHeader() {
  264. // Parse the contents with a DOM parser
  265. return new tinymce.html.DomParser({
  266. validate: false,
  267. root_name: '#document'
  268. }).parse(head);
  269. }
  270. function setContent(evt) {
  271. var startPos, endPos, content = evt.content, headerFragment, styles = '', dom = editor.dom, elm;
  272. if (evt.selection) {
  273. return;
  274. }
  275. function low(s) {
  276. return s.replace(/<\/?[A-Z]+/g, function(a) {
  277. return a.toLowerCase();
  278. });
  279. }
  280. // Ignore raw updated if we already have a head, this will fix issues with undo/redo keeping the head/foot separate
  281. if (evt.format == 'raw' && head) {
  282. return;
  283. }
  284. if (evt.source_view && editor.getParam('fullpage_hide_in_source_view')) {
  285. return;
  286. }
  287. // Parse out head, body and footer
  288. content = content.replace(/<(\/?)BODY/gi, '<$1body');
  289. startPos = content.indexOf('<body');
  290. if (startPos != -1) {
  291. startPos = content.indexOf('>', startPos);
  292. head = low(content.substring(0, startPos + 1));
  293. endPos = content.indexOf('</body', startPos);
  294. if (endPos == -1) {
  295. endPos = content.length;
  296. }
  297. evt.content = content.substring(startPos + 1, endPos);
  298. foot = low(content.substring(endPos));
  299. } else {
  300. head = getDefaultHeader();
  301. foot = '\n</body>\n</html>';
  302. }
  303. // Parse header and update iframe
  304. headerFragment = parseHeader();
  305. each(headerFragment.getAll('style'), function(node) {
  306. if (node.firstChild) {
  307. styles += node.firstChild.value;
  308. }
  309. });
  310. elm = headerFragment.getAll('body')[0];
  311. if (elm) {
  312. dom.setAttribs(editor.getBody(), {
  313. style: elm.attr('style') || '',
  314. dir: elm.attr('dir') || '',
  315. vLink: elm.attr('vlink') || '',
  316. link: elm.attr('link') || '',
  317. aLink: elm.attr('alink') || ''
  318. });
  319. }
  320. dom.remove('fullpage_styles');
  321. var headElm = editor.getDoc().getElementsByTagName('head')[0];
  322. if (styles) {
  323. dom.add(headElm, 'style', {
  324. id : 'fullpage_styles'
  325. }, styles);
  326. // Needed for IE 6/7
  327. elm = dom.get('fullpage_styles');
  328. if (elm.styleSheet) {
  329. elm.styleSheet.cssText = styles;
  330. }
  331. }
  332. var currentStyleSheetsMap = {};
  333. tinymce.each(headElm.getElementsByTagName('link'), function(stylesheet) {
  334. if (stylesheet.rel == 'stylesheet' && stylesheet.getAttribute('data-mce-fullpage')) {
  335. currentStyleSheetsMap[stylesheet.href] = stylesheet;
  336. }
  337. });
  338. // Add new
  339. tinymce.each(headerFragment.getAll('link'), function(stylesheet) {
  340. var href = stylesheet.attr('href');
  341. if (!currentStyleSheetsMap[href] && stylesheet.attr('rel') == 'stylesheet') {
  342. dom.add(headElm, 'link', {
  343. rel: 'stylesheet',
  344. text: 'text/css',
  345. href: href,
  346. 'data-mce-fullpage': '1'
  347. });
  348. }
  349. delete currentStyleSheetsMap[href];
  350. });
  351. // Delete old
  352. tinymce.each(currentStyleSheetsMap, function(stylesheet) {
  353. stylesheet.parentNode.removeChild(stylesheet);
  354. });
  355. }
  356. function getDefaultHeader() {
  357. var header = '', value, styles = '';
  358. if (editor.getParam('fullpage_default_xml_pi')) {
  359. header += '<?xml version="1.0" encoding="' + editor.getParam('fullpage_default_encoding', 'ISO-8859-1') + '" ?>\n';
  360. }
  361. header += editor.getParam('fullpage_default_doctype', '<!DOCTYPE html>');
  362. header += '\n<html>\n<head>\n';
  363. if ((value = editor.getParam('fullpage_default_title'))) {
  364. header += '<title>' + value + '</title>\n';
  365. }
  366. if ((value = editor.getParam('fullpage_default_encoding'))) {
  367. header += '<meta http-equiv="Content-Type" content="text/html; charset=' + value + '" />\n';
  368. }
  369. if ((value = editor.getParam('fullpage_default_font_family'))) {
  370. styles += 'font-family: ' + value + ';';
  371. }
  372. if ((value = editor.getParam('fullpage_default_font_size'))) {
  373. styles += 'font-size: ' + value + ';';
  374. }
  375. if ((value = editor.getParam('fullpage_default_text_color'))) {
  376. styles += 'color: ' + value + ';';
  377. }
  378. header += '</head>\n<body' + (styles ? ' style="' + styles + '"' : '') + '>\n';
  379. return header;
  380. }
  381. function getContent(evt) {
  382. if (!evt.selection && (!evt.source_view || !editor.getParam('fullpage_hide_in_source_view'))) {
  383. evt.content = tinymce.trim(head) + '\n' + tinymce.trim(evt.content) + '\n' + tinymce.trim(foot);
  384. }
  385. }
  386. editor.addCommand('mceFullPageProperties', showDialog);
  387. editor.addButton('fullpage', {
  388. title: 'Document properties',
  389. cmd : 'mceFullPageProperties'
  390. });
  391. editor.addMenuItem('fullpage', {
  392. text: 'Document properties',
  393. cmd : 'mceFullPageProperties',
  394. context: 'file'
  395. });
  396. editor.on('BeforeSetContent', setContent);
  397. editor.on('GetContent', getContent);
  398. });