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