Commit 9c1a42b8 by cahrens

Use postMessage to communicate between the iframe and TinyMCE.

This gets around cross-origin issues on the CDN.

Delete plugin.min.js as the Jake command will minimize into the full file.
parent fe847720
......@@ -63,8 +63,8 @@ class @HTMLEditingDescriptor
ed.on('EditImage', @editImage)
ed.on('SaveLink', @saveLink)
ed.on('EditLink', @editLink)
ed.on('ShowCodeMirror', @showCodeEditor)
ed.on('SaveCodeMirror', @saveCodeEditor)
ed.on('ShowCodeEditor', @showCodeEditor)
ed.on('SaveCodeEditor', @saveCodeEditor)
editImage: (data) =>
# Called when the image plugin will be shown. Input arg is the JSON version of the image data.
......@@ -86,17 +86,17 @@ class @HTMLEditingDescriptor
if data['href']
data['href'] = rewriteStaticLinks(data['href'], '/static/', @base_asset_url)
showCodeEditor: (codeEditor) =>
showCodeEditor: (source) =>
# Called when the CodeMirror Editor is displayed to convert links to show static prefix.
# The input argument is the CodeMirror instance.
content = rewriteStaticLinks(codeEditor.getValue(), @base_asset_url, '/static/')
codeEditor.setValue(content)
# The input argument is a dict with the text content.
content = rewriteStaticLinks(source.content, @base_asset_url, '/static/')
source.content = content
saveCodeEditor: (codeEditor) =>
saveCodeEditor: (source) =>
# Called when the CodeMirror Editor is saved to convert links back to the full form.
# The input argument is the CodeMirror instance.
content = rewriteStaticLinks(codeEditor.getValue(), '/static/', @base_asset_url)
codeEditor.setValue(content)
# The input argument is a dict with the text content.
content = rewriteStaticLinks(source.content, '/static/', @base_asset_url)
source.content = content
initInstanceCallback: (visualEditor) =>
visualEditor.setContent(rewriteStaticLinks(visualEditor.getContent({no_events: 1}), '/static/', @base_asset_url))
......
......@@ -18,24 +18,114 @@ tinymce.PluginManager.add('codemirror', function(editor, url) {
editor.selection.collapse(true);
editor.selection.setContent('<span class="CmCaReT" style="display:none">&#0;</span>');
// Determine the origin of the window that will host the code editor.
// If tinyMCE's baseURL is relative, then static files are hosted in the
// same origin as the containing page. If it is not relative, then we know that
// the origin of the iframe hosting the code editor will match the origin
// of tinyMCE's baseURL, as they are both hosted on the CDN.
var codeEditorOrigin;
var index = tinyMCE.baseURL.indexOf("/static/");
if (index > 0) {
codeEditorOrigin = tinyMCE.baseURL.substring(0, index);
}
else {
codeEditorOrigin = window.location.origin;
}
// Send the path location for CodeMirror and the parent origin to use in postMessage.
var sourceHtmlParams = "?CodeMirrorPath=" + editor.settings.codemirror.path +
"&ParentOrigin=" + window.location.origin;
// Open editor window
var win = editor.windowManager.open({
title: 'HTML source code',
url: url + '/source.html',
width: 800,
height: 550,
resizable : true,
maximizable : true,
buttons: [
{ text: 'Ok', subtype: 'primary', onclick: function(){
var doc = document.querySelectorAll('.mce-container-body>iframe')[0];
doc.contentWindow.submit();
win.close();
}},
{ text: 'Cancel', onclick: 'close' }
]
});
};
url: url + '/source.html' + sourceHtmlParams,
width: 800,
height: 550,
resizable: true,
maximizable: true,
buttons: [
{ text: 'OK', subtype: 'primary', onclick: function () {
postToCodeEditor({type: "save"});
}},
{ text: 'Cancel', onclick: function () {
postToCodeEditor({type: "cancel"});
}}
]
});
// The master version of TinyMCE has a win.getContentWindow() method. This is its implementation.
var codeWindow = win.getEl().getElementsByTagName('iframe')[0].contentWindow;
var postToCodeEditor = function (data) {
codeWindow.postMessage(data, codeEditorOrigin);
};
var messageListener = function (event) {
// Check that the message came from the code editor.
if (codeEditorOrigin !== event.origin) {
return;
}
var source;
if (event.data.type === "init") {
source = {content: editor.getContent({source_view: true})};
// Post an event to allow rewriting of static links on the content.
editor.fire("ShowCodeEditor", source);
postToCodeEditor(
{
type: "init",
content: source.content
}
);
editor.dom.remove(editor.dom.select('.CmCaReT'));
}
else if (event.data.type === "setText") {
source = {content: event.data.text};
var isDirty = event.data.isDirty;
// Post an event to allow rewriting of static links on the content.
editor.fire('SaveCodeEditor', source);
editor.setContent(source.content);
// Set cursor:
var el = editor.dom.select('span#CmCaReT')[0];
if (el) {
editor.selection.scrollIntoView(el);
editor.selection.setCursorLocation(el,0);
editor.dom.remove(el);
}
// EDX: added because CmCaReT span was getting left in when caret was within a style tag.
// Make sure to strip it out (and accept that caret will not be in the correct place).
else {
var content = editor.getContent();
var strippedContent = content.replace('<span id="CmCaReT"></span>', '');
if (content !== strippedContent) {
editor.setContent(strippedContent);
}
}
// EDX: moved block of code from original location since we may change content in bug fix code above.
editor.isNotDirty = !isDirty;
if (isDirty) {
editor.nodeChanged();
}
}
else if (event.data.type === "closeWindow") {
win.close();
}
};
win.on("close", function() {
window.removeEventListener("message", messageListener);
});
window.addEventListener("message", messageListener, false);
}
// Add a button to the button bar
// EDX changed to show "HTML" on toolbar button
......
tinymce.PluginManager.requireLangPack("codemirror");
tinymce.PluginManager.add("codemirror",function(a,c){function b(){a.focus();a.selection.collapse(!0);a.selection.setContent('<span class="CmCaReT" style="display:none">&#0;</span>');var b=a.windowManager.open({title:"HTML source code",url:c+"/source.html",width:800,height:550,resizable:!0,maximizable:!0,buttons:[{text:"Ok",subtype:"primary",onclick:function(){document.querySelectorAll(".mce-container-body>iframe")[0].contentWindow.submit();b.close()}},{text:"Cancel",onclick:"close"}]})}a.addButton("code",
{title:"Edit HTML",text:"HTML",icon:false,onclick:b});a.addMenuItem("code",{icon:"code",text:"Edit HTML",context:"tools",onclick:b})});
<!DOCTYPE html>
<html>
<head>
<!--Necessary to set encoding when running on CloudFront CDN. -->
<meta charset="UTF-8">
<script>
/**
......@@ -11,40 +13,75 @@
*/
// Global vars:
var tinymce, // Reference to TinyMCE
editor, // Reference to TinyMCE editor
codemirror, // CodeMirror instance
chr = 0, // Unused utf-8 character, placeholder for cursor
isMac = /macintosh|mac os/i.test(navigator.userAgent),
CMsettings; // CodeMirror settings
function inArray(key, arr)
{
"use strict";
arr = '|' + arr.join('|') + '|';
return arr.indexOf('|'+key+'|') != -1;
var codemirror, // CodeMirror instance
chr = 0, // Unused utf-8 character, placeholder for cursor
isMac = /macintosh|mac os/i.test(navigator.userAgent),
CMsettings, // CodeMirror settings
messageHandler,
parentOrigin,
postToParent;
messageHandler = function (event) {
if (event.origin !== parentOrigin) {
return;
}
var cleanup = function () {
window.removeEventListener("message", messageHandler);
postToParent({type: "closeWindow"});
};
if (event.data.type === "init") {
start(event.data.content);
}
else if (event.data.type == "save") {
submit();
cleanup();
}
else if (event.data.type == "cancel") {
cleanup();
}
};
postToParent = function(data) {
parent.postMessage(data, parentOrigin);
};
window.addEventListener("message", messageHandler);
function inArray(key, arr) {
"use strict";
arr = '|' + arr.join('|') + '|';
return arr.indexOf('|' + key + '|') != -1;
}
(function()
{// Initialise (before load)
"use strict";
tinymce = parent.tinymce;
editor = tinymce.activeEditor;
var i, userSettings = editor.settings.codemirror ? editor.settings.codemirror : null;
CMsettings = {
path: userSettings.path || 'CodeMirror',
indentOnInit: userSettings.indentOnInit || false,
config: {// Default config
mode: 'htmlmixed',
lineNumbers: true,
lineWrapping: true,
indentUnit: 1,
tabSize: 1,
matchBrackets: true,
styleActiveLine: true
},
jsFiles: [// Default JS files
(function () { // Initialise (before load)
"use strict";
var getParamByName = function (name) {
var match = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
};
// Getting this as a query parameter is very ugly, but the JS and CSS needs
// to be added to the head as the page is being rendered. Therefore it isn't possible
// to use postMessage APIs to get the setting value. And we can't hardcode the value in here
// because we need the baseURL, which is only added to server-side templated files.
var codemirrorPath = getParamByName("CodeMirrorPath");
parentOrigin = getParamByName("ParentOrigin");
CMsettings = {
path: codemirrorPath,
indentOnInit: false,
config: {// Default config
mode: 'htmlmixed',
lineNumbers: true,
lineWrapping: true,
indentUnit: 1,
tabSize: 1,
matchBrackets: true,
styleActiveLine: true
},
jsFiles: [// Default JS files
// MODIFIED FOR EDX VENDOR FILE LOCATIONS.
'codemirror-compressed.js',
// 'addon/edit/matchbrackets.js', Chose not to enable
......@@ -52,64 +89,47 @@ function inArray(key, arr)
// 'mode/javascript/javascript.js', In compressed
// 'mode/css/css.js', In compressed
// 'mode/htmlmixed/htmlmixed.js', In compressed
'CodeMirror/addons/dialog/dialog.js'
'CodeMirror/addons/dialog/dialog.js'
// 'addon/search/searchcursor.js', In compressed
// 'addon/search/search.js', In compressed
// 'addon/selection/active-line.js' Chose not to enable
],
cssFiles: [
'CodeMirror/codemirror.css',
'CodeMirror/addons/dialog/dialog.css'
]
};
// Merge config
for (i in userSettings.config) {
CMsettings.config[i] = userSettings.config[i];
}
// Merge jsFiles
for (i in userSettings.jsFiles) {
if (!inArray(userSettings.jsFiles[i], CMsettings.jsFiles)) {
CMsettings.jsFiles.push(userSettings.jsFiles[i]);
}
}
// Merge cssFiles
for (i in userSettings.cssFiles) {
if (!inArray(userSettings.cssFiles[i], CMsettings.cssFiles)) {
CMsettings.cssFiles.push(userSettings.cssFiles[i]);
}
}
// Add trailing slash to path
if (!/\/$/.test(CMsettings.path)) {
CMsettings.path += '/';
}
// Write stylesheets
for (i = 0; i < CMsettings.cssFiles.length; i++) {
document.write('<li'+'nk rel="stylesheet" type="text/css" href="' + CMsettings.path + CMsettings.cssFiles[i] + '" />');
}
// Write JS source files
for (i = 0; i < CMsettings.jsFiles.length; i++) {
document.write('<scr'+'ipt type="text/javascript" src="' + CMsettings.path + CMsettings.jsFiles[i] + '"></scr'+'ipt>');
}
window.onload = start;
],
cssFiles: [
'CodeMirror/codemirror.css',
'CodeMirror/addons/dialog/dialog.css'
]
};
// Add trailing slash to path
if (!/\/$/.test(CMsettings.path)) {
CMsettings.path += '/';
}
// Write stylesheets
for (var i = 0; i < CMsettings.cssFiles.length; i++) {
document.write('<li' + 'nk rel="stylesheet" type="text/css" href="' + CMsettings.path + CMsettings.cssFiles[i] + '" />');
}
// Write JS source files
for (var j = 0; j < CMsettings.jsFiles.length; j++) {
document.write('<scr' + 'ipt type="text/javascript" src="' + CMsettings.path + CMsettings.jsFiles[j] + '"></scr' + 'ipt>');
}
window.onload = function () {
postToParent({type: "init"});
};
}());
function start()
{// Initialise (on load)
"use strict";
function start(html) {// Initialise (on load)
"use strict";
if (typeof(window.CodeMirror) !== 'function') {
alert('CodeMirror not found in "' + CMsettings.path + '", aborting...');
return;
}
if (typeof(window.CodeMirror) !== 'function') {
alert('CodeMirror not found in "' + CMsettings.path + '", aborting...');
return;
}
// Create legend for keyboard shortcuts for find & replace:
// Create legend for keyboard shortcuts for find & replace:
// Disabled by EDX.
// var head = parent.document.querySelectorAll('.mce-foot')[0],
// div = parent.document.createElement('div'),
......@@ -120,131 +140,105 @@ function start()
// div.style.left = div.style.bottom = '5px';
// head.appendChild(div);
// Set CodeMirror cursor to same position as cursor was in TinyMCE:
var html = editor.getContent({source_view: true});
html = html.replace(/<span\s+class="CmCaReT"([^>]*)>([^<]*)<\/span>/gm, String.fromCharCode(chr));
editor.dom.remove(editor.dom.select('.CmCaReT'));
CodeMirror.defineInitHook(function(inst)
{
// EDX: added to switch static links.
editor.fire("ShowCodeMirror", inst);
// Move cursor to correct position:
inst.focus();
var cursor = inst.getSearchCursor(String.fromCharCode(chr), false);
if (cursor.findNext()) {
inst.setCursor(cursor.to());
cursor.replace('');
}
// Indent all code, if so requested:
if (editor.settings.codemirror.indentOnInit) {
var last = inst.lineCount();
inst.operation(function() {
for (var i = 0; i < last; ++i) {
inst.indentLine(i);
}
});
}
});
CMsettings.config.value = html;
// Instantiante CodeMirror:
codemirror = CodeMirror(document.body, CMsettings.config);
codemirror.isDirty = false;
codemirror.on('change', function(inst) {
inst.isDirty = true;
});
// Set CodeMirror cursor to same position as cursor was in TinyMCE:
html = html.replace(/<span\s+class="CmCaReT"([^>]*)>([^<]*)<\/span>/gm, String.fromCharCode(chr));
CodeMirror.defineInitHook(function (inst) {
// Move cursor to correct position:
inst.focus();
var cursor = inst.getSearchCursor(String.fromCharCode(chr), false);
if (cursor.findNext()) {
inst.setCursor(cursor.to());
cursor.replace('');
}
// Indent all code, if so requested:
if (CMsettings.indentOnInit) {
var last = inst.lineCount();
inst.operation(function () {
for (var i = 0; i < last; ++i) {
inst.indentLine(i);
}
});
}
});
CMsettings.config.value = html;
// Instantiante CodeMirror:
codemirror = CodeMirror(document.body, CMsettings.config);
codemirror.isDirty = false;
codemirror.on('change', function (inst) {
inst.isDirty = true;
});
}
function findDepth(haystack, needle)
{
"use strict";
var idx = haystack.indexOf(needle), depth = 0, x;
for (x = idx; x >= 0; x--) {
switch(haystack.charAt(x)) {
case '<': depth--; break;
case '>': depth++; break;
}
}
return depth;
function findDepth(haystack, needle) {
"use strict";
var idx = haystack.indexOf(needle), depth = 0, x;
for (x = idx; x >= 0; x--) {
switch (haystack.charAt(x)) {
case '<':
depth--;
break;
case '>':
depth++;
break;
}
}
return depth;
}
// This function is called by plugin.js, when user clicks 'Ok' button
function submit()
{
"use strict";
var cc = '&#x0;', isDirty = codemirror.isDirty, doc = codemirror.doc;
if (doc.somethingSelected()) {
// Clear selection:
doc.setCursor(doc.getCursor());
}
// Insert cursor placeholder (&#x0;)
doc.replaceSelection(cc);
var pos = codemirror.getCursor(),
curLineHTML = doc.getLine(pos.line);
if (findDepth(curLineHTML, cc) !== 0) {
// Cursor is inside a <tag>, don't set cursor:
curLineHTML = curLineHTML.replace(cc, '');
doc.setLine(pos.line, curLineHTML);
}
// Submit HTML to TinyMCE:
// EDX: added to switch static links.
editor.fire('SaveCodeMirror', codemirror);
editor.setContent(codemirror.getValue().replace(cc, '<span id="CmCaReT"></span>'));
// Set cursor:
var el = editor.dom.select('span#CmCaReT')[0];
if (el) {
editor.selection.scrollIntoView(el);
editor.selection.setCursorLocation(el,0);
editor.dom.remove(el);
}
// EDX: added because CmCaReT span was getting left in when caret was within a style tag.
// Make sure to strip it out (and accept that caret will not be in the correct place).
else {
var content = editor.getContent();
var strippedContent = content.replace('<span id="CmCaReT"></span>', '');
if (content !== strippedContent) {
editor.setContent(strippedContent);
}
function submit() {
"use strict";
var cc = '&#x0;', isDirty = codemirror.isDirty, doc = codemirror.doc;
if (doc.somethingSelected()) {
// Clear selection:
doc.setCursor(doc.getCursor());
}
// EDX: moved block of code from original location since we may change content in bug fix code above.
editor.isNotDirty = !isDirty;
if (isDirty) {
editor.nodeChanged();
}
// Insert cursor placeholder (&#x0;)
doc.replaceSelection(cc);
var pos = codemirror.getCursor(),
curLineHTML = doc.getLine(pos.line);
if (findDepth(curLineHTML, cc) !== 0) {
// Cursor is inside a <tag>, don't set cursor:
curLineHTML = curLineHTML.replace(cc, '');
doc.setLine(pos.line, curLineHTML);
}
postToParent(
{
type: "setText",
text: codemirror.getValue().replace(cc, '<span id="CmCaReT"></span>'),
isDirty: isDirty
}
);
}
</script>
<style type="text/css">
body {
margin: 0;
}
body {
margin: 0;
}
.CodeMirror {
height: 100%;
font-size: 12px;
line-height: 18px;
}
.CodeMirror {
height: 100%;
font-size: 12px;
line-height: 18px;
}
.CodeMirror-activeline-background {
background: #e8f2ff !important;
}
.CodeMirror-activeline-background {
background: #e8f2ff !important;
}
</style>
</head>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment