Commit 3b2ee980 authored by Jérome Perrin's avatar Jérome Perrin

erp5: enable monaco as a code editor

parent babc162c
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>monaco_editor_support</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<tal:block tal:condition="options/field_id | nothing">
<tal:comment tal:replace="nothing">When rendered as an ERP5 field, we need to add a textarea and adjust CSS.
</tal:comment>
<textarea tal:attributes="id options/field_id;
name options/field_id"
style="display:none"
tal:content="options/content">
</textarea>
<style>
/* Override some conflicting default erp5.css styles */
/* - font size and indentation rules */
span {
font-family: unset !important;
}
div#monaco-container * {
font-family: Menlo, Monaco, "Courier New", monospace;
font-size: unset !important;
}
/* - selected text highlight */
.monaco-editor .view-lines {
background-color: transparent !important;
}
/* - popup menu */
.monaco-action-bar .action-label.disabled {
height: 0px;
}
.monaco-menu li.action-item a.action-label {
font-family: "Segoe WPC", "Segoe UI", ".SFNSDisplay-Light", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback" !important;
}
/* - Command palette (FIXME: background color inherits ERP5 page background color) */
.quick-open-entry * {
font-family: "Segoe WPC", "Segoe UI", ".SFNSDisplay-Light", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback" !important;
}
</style>
</tal:block>
<div id="monaco-container" style="width:100%;height:800px;border:1px solid grey;"></div>
<script tal:content='python: "var portal_url=" + modules["json"].dumps(options.get("portal_url"))'></script>
<script tal:content='python: "var field_id=" + modules["json"].dumps(options.get("field_id"))'></script>
<script tal:content='python: "var mode=" + modules["json"].dumps(options["mode"])'></script>
<script tal:content='python: "var textarea_selector=" + modules["json"].dumps(options.get("textarea_selector"))'>
</script>
<script tal:content='python: "var bound_names=" + modules["json"].dumps(options.get("bound_names"))'></script>
<script
tal:content='python: "window.monacoEditorWebPackResourceBaseUrl = " + modules["json"].dumps(options["portal_url"]) + " + \"/monaco-editor/\""'>
</script>
<script charset="utf-8">
/* we need to defer import for the monacoEditorWebPackResourceBaseUrl trick to work as expected in ZMI */
var $script = document.createElement("script");
$script.src =
window.monacoEditorWebPackResourceBaseUrl + "/monaco-editor/app.bundle.min.js";
document.head.appendChild($script);
$script.onload = function() {
var $textarea =
document.querySelector(textarea_selector) ||
document.getElementById(field_id);
if (textarea_selector) {
/* ZMI mode */
/* create a div instead of the default textarea */
var $monacoContainer = document.getElementById("monaco-container");
$monacoContainer.parentNode.removeChild($monacoContainer);
$textarea.parentNode.appendChild($monacoContainer);
$monacoContainer.style.width = $textarea.parentNode.offsetWidth - 10 + "px";
$monacoContainer.style.height = $textarea.parentNode.offsetHeight + "px";
$textarea.style.display = "none";
function saveDocument() {
var $saveButton = document.querySelector('input[value="Save Changes"]');
$saveButton.click();
return false;
}
} else {
/* ERP5 editor field mode */
/* all ERP5 field have a .title that shows a popup, we don't want this popup on this editor */
$textarea.parentNode.title = "";
function saveDocument() {
clickSaveButton("Base_edit");
document.getElementById("main_form").submit();
}
}
// this is codemorrir only
if (mode === "htmlmixed") {
mode = "html";
}
var editor = monaco.editor.create(
document.getElementById("monaco-container"),
{
value: $textarea.value,
language: mode,
/* because Alt+Click is LeftClick on ChromeOS */
multiCursorModifier: "ctrlCmd",
autoIndent: true
}
);
if (mode == "python") {
editor.getModel().updateOptions({ tabSize: 2 });
}
if (mode === "html") {
monaco.languages.html.htmlDefaults.options.format.tabSize = 2;
monaco.languages.html.htmlDefaults.options.format.insertSpaces = true;
}
var timeout = null;
function checkPythonSourceCode() {
const data = new FormData();
const checker_parameters = {
code: editor.getValue()
};
// ZMI python scripts pass extra parameters to linter
if (bound_names) {
checker_parameters["bound_names"] = JSON.parse(bound_names);
checker_parameters["params"] = document.querySelector(
'input[name="params"]'
).value;
}
data.append("data", JSON.stringify(checker_parameters));
fetch(portal_url + "/ERP5Site_checkPythonSourceCodeAsJSON", {
method: "POST",
body: data
})
.then(response => response.json())
.then(data => {
monaco.editor.setModelMarkers(
editor.getModel(),
"pylint",
data["annotations"].map(annotation => {
return {
startLineNumber: annotation.row + 1,
endLineNumber: annotation.row + 1,
startColumn: annotation.col,
endColumn: Infinity,
message: annotation.text,
severity:
annotation.type === "error"
? monaco.MarkerSeverity.Error
: monaco.MarkerSeverity.Warning
};
})
);
timeout = null;
});
}
editor.getModel().onDidChangeContent(event => {
$textarea.value = editor.getValue();
changed = true; /* global variable used in erp5.js for onbeforeunload event */
if (mode == "python") {
// debounced `checkPythonSourceCode`
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(checkPythonSourceCode, 300);
}
});
if (mode === "python") {
// Perform a first check when loading document.
checkPythonSourceCode();
}
editor.addAction({
id: "save",
label: "Save",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S],
precondition: null,
keybindingContext: null,
contextMenuGroupId: "navigation",
contextMenuOrder: 1.5,
run: function(ed) {
return saveDocument();
}
});
};
</script>
\ No newline at end of file
......@@ -6,4 +6,7 @@ if getattr(context.portal_skins, "erp5_ace_editor", None) is not None:
if getattr(context.portal_skins, "erp5_code_mirror", None) is not None:
editor_list.append(("Code Mirror", "codemirror"))
if getattr(context.portal_skins, "erp5_monaco_editor", None) is not None:
editor_list.append(("Monaco Editor", "monaco"))
return editor_list
......@@ -119,6 +119,24 @@ class EditorWidget(Widget.TextAreaWidget):
return ace_editor_support.pt_render(extra_context={'field': field,
'content': value,
'id': key})
elif text_editor == 'monaco':
monaco_editor_support = getattr(here, 'monaco_editor_support', None)
if monaco_editor_support is not None:
mode = "python"
portal_type = here.getPortalType()
if portal_type == "Web Page":
mode = "htmlmixed"
elif portal_type == "Web Script":
mode = "javascript"
elif portal_type == "Web Style":
mode = "css"
site_root = here.getWebSiteValue() or here.getPortalObject()
return monaco_editor_support(
field=field,
content=value,
field_id=key,
portal_url=site_root.absolute_url(),
mode=mode)
elif text_editor == 'codemirror':
code_mirror_support = getattr(here, 'code_mirror_support', None)
if code_mirror_support is not None:
......
......@@ -19,9 +19,6 @@ def manage_page_footer(self):
except:
editor = None
if editor not in ('ace', 'codemirror'):
return default
# REQUEST['PUBLISHED'] can be the form in the acquisition context of the
# document, or a method bound to the document (after a POST it is a bound method)
published = self.REQUEST.get('PUBLISHED')
......@@ -92,7 +89,15 @@ def manage_page_footer(self):
portal_url=portal_url,
bound_names=bound_names,
mode=mode))
else:
elif editor == 'monaco' and getattr(portal, 'monaco_editor_support', None) is not None:
return '''%s
</body>
</html>''' % (portal.monaco_editor_support(
textarea_selector=textarea_selector,
portal_url=portal_url,
bound_names=bound_names,
mode=mode).encode('utf-8'))
elif editor == 'ace':
return '''
<script type="text/javascript" src="%(portal_url)s/jquery/core/jquery.min.js"></script>
<script type="text/javascript" src="%(portal_url)s/ace/ace.js"></script>
......@@ -178,4 +183,6 @@ $(document).ready(function() {
</body>
</html>''' % locals()
return default
Navigation.manage_page_footer = manage_page_footer
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