Commit 3f5376be by Arnaud Fontaine

ZODB Components: Add CodeMirror editor.

CodeMirror API is much more cleaner (from code and design point of view) than
Ace Editor and include useful plugins, such as MergeView supported in ERP5 to
diff with previous versions.
1 parent 6560ac6d
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="DTMLDocument" module="OFS.DTMLDocument"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>code_mirror_support</string> </value>
</item>
<item>
<key> <string>_vars</string> </key>
<value>
<dictionary/>
</value>
</item>
<item>
<key> <string>globals</string> </key>
<value>
<dictionary/>
</value>
</item>
<item>
<key> <string>raw</string> </key>
<value> <string encoding="cdata"><![CDATA[
<script type="text/javascript" src="&dtml-portal_url;/codemirror/lib/codemirror.js"></script>\n
<link rel="stylesheet" href="&dtml-portal_url;/codemirror/lib/codemirror.css">\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/mode/python/python.js"></script>\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/edit/matchbrackets.js"></script>\n
\n
<!-- Trailing spaces -->\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/edit/trailingspace.js"></script>\n
<style type="text/css">\n
.cm-trailingspace {\n
background-color: gray;\n
}\n
</style>\n
\n
<!-- Search addons -->\n
<link rel="stylesheet" href="&dtml-portal_url;/codemirror/addon/dialog/dialog.css">\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/dialog/dialog.js"></script>\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/search/searchcursor.js"></script>\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/search/search.js"></script>\n
\n
<!-- Python autocomplete (Ctrl-Space, see below)\n
TODO-arnau: Add ERP5 autocompletion?\n
-->\n
<link rel="stylesheet" href="&dtml-portal_url;/codemirror/addon/hint/show-hint.css">\n
<script src="&dtml-portal_url;/codemirror/addon/hint/show-hint.js"></script>\n
<script src="&dtml-portal_url;/codemirror/addon/hint/python-hint.js"></script>\n
\n
<!-- Code folding -->\n
<link rel="stylesheet" href="&dtml-portal_url;/codemirror/addon/fold/foldgutter.css">\n
<script src="&dtml-portal_url;/codemirror/addon/fold/foldcode.js"></script>\n
<script src="&dtml-portal_url;/codemirror/addon/fold/foldgutter.js"></script>\n
<script src="&dtml-portal_url;/codemirror/addon/fold/indent-fold.js"></script>\n
<script src="&dtml-portal_url;/codemirror/addon/fold/comment-fold.js"></script>\n
\n
<!-- Merge -->\n
<link rel="stylesheet" href="&dtml-portal_url;/codemirror/addon/merge/merge.css">\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/merge/dep/diff_match_patch.js"></script>\n
<script type="text/javascript" src="&dtml-portal_url;/codemirror/addon/merge/merge.js"></script>\n
\n
<style type="text/css">\n
.maximize_fullscreen_message {\n
display: table;\n
position: absolute;\n
bottom: 0;\n
right: 20px;\n
width: 40%;\n
z-index: 424242;\n
padding: 20px;\n
background-color: #DAE6F6;\n
border: 1px solid #97B0D1;\n
opacity: 0.3;\n
cursor: pointer;\n
font-weight: bold;\n
}\n
\n
.maximize_fullscreen_error_message {\n
background-color: red;\n
}\n
\n
.maximize_fullscreen_message > div {\n
font-size: 14px;\n
display: table-cell;\n
vertical-align: middle;\n
}\n
\n
#maximize_message {\n
display: block !important;\n
position: absolute !important;\n
bottom: 0 !important;\n
right: 0px !important;\n
z-index: 4243 !important;\n
padding: 10px;\n
font-size: 16px;\n
font-weight: bold;\n
background-color: black;\n
color: white;\n
}\n
\n
.maximize {\n
position: fixed;\n
right: 0;\n
bottom: 0;\n
top: 0 !important;\n
left: 0 !important;\n
z-index: 4242 !important;\n
overflow: hidden !important;\n
}\n
</style>\n
\n
<input type="button" value="Maximize" onclick="maximize()"\n
class="editor_action_button" />\n
<input type="button" value="Fullscreen" onclick="switchToFullScreen(cm)"\n
class="editor_action_button" />\n
\n
<div id="merge" style="height: 100%; width: 100%">\n
<div id="view" style="display: none;"></div>\n
\n
<textarea id="&dtml-field_id;" name="&dtml-field_id;" style="display: none;">\n
<dtml-var content>\n
</textarea>\n
</div>\n
\n
<script type="text/javascript">\n
error_element = $(\'div.input > .error\');\n
error_arr = [];\n
warning_element = $(\'div.input > .warning\');\n
warning_arr = [];\n
merge_mode_elem = null;\n
\n
maximize_mode_message = $(\'<span id="maximize_message">Press ESC to leave maximize mode</span>\');\n
\n
function maximizeFullscreenRemoveSaveMessage() {\n
$(\'.maximize_fullscreen_message\').remove();\n
}\n
\n
function updateErrorWarningMessageDivWithJump() {\n
if(!error_element.length && !warning_element.length)\n
return;\n
\n
function getErrorWarningMessageDictHandler(data) {\n
error_warning_dict = $.parseJSON(data);\n
\n
function fillMessageElementAndArray(list, elem, arr) {\n
$.each(list, function(i, dict) {\n
line = dict[\'line\'];\n
column = dict[\'column\'];\n
if(line != null && column != null)\n
arr.push(\'<a href="#" \' +\n
\' onclick="cm.setCursor(\' + (line - 1) + \', \' + column + \');\' +\n
\'cm.focus(); event.stopPropagation(); event.preventDefault();">\' +\n
dict[\'message\'] +\n
\'</a>\');\n
else\n
arr.push(dict[\'message\']);\n
});\n
\n
elem.html(arr.join(\'<br />\'));\n
}\n
\n
if(error_element.length) {\n
error_arr.length = 0;\n
fillMessageElementAndArray(error_warning_dict[\'error_list\'],\n
error_element, error_arr);\n
}\n
\n
if(warning_element.length) {\n
warning_arr.length = 0;\n
fillMessageElementAndArray(error_warning_dict[\'warning_list\'],\n
warning_element, warning_arr);\n
}\n
}\n
\n
$.ajax({type: \'GET\',\n
async: false,\n
url: \'Component_getErrorWarningMessageDictAsJson\',\n
success: getErrorWarningMessageDictHandler});\n
}\n
\n
// Save source code only through an AJAX request\n
function saveDocument(cm, event) {\n
event.stopPropagation();\n
event.preventDefault();\n
\n
clickSaveButton(\'Base_edit\');\n
\n
/* If the save is successful, then update validation state field (requires\n
* validation_state CSS class to be set on the field) and error\n
* message (requires error CSS class to be set on the field) on the main\n
* page. If inside maximize/fullscreen mode, display an box with the\n
* result as well\n
*/\n
function successHandler(data) {\n
transition_message = $(\'#transition_message\');\n
transition_message.css(\'opacity\', 0.0);\n
transition_message.html(data);\n
transition_message.animate({opacity: 1.0},\n
{duration: 3000, queue: false});\n
\n
var maximize_fullscreen_message = data;\n
\n
var validation_state_span = $(\'div.input > .validation_state\');\n
if(validation_state_span.length) {\n
// Animate field to emphasize the change\n
function getTranslatedValidationStateTitleHandler(data) {\n
validation_state_span.css(\'opacity\', 0.0);\n
validation_state_span.html(data);\n
validation_state_span.animate({opacity: 1.0},\n
{duration: 3000, queue: false});\n
}\n
\n
$.ajax({type: \'GET\',\n
url: \'getTranslatedValidationStateTitle\',\n
success: getTranslatedValidationStateTitleHandler});\n
}\n
\n
updateErrorWarningMessageDivWithJump();\n
\n
// Animate fields to emphasize the change\n
if(error_element.length) {\n
error_element.css(\'opacity\', 0.0);\n
error_element.animate({opacity: 1.0}, {duration: 3000, queue: false});\n
}\n
\n
if(warning_element.length) {\n
warning_element.css(\'opacity\', 0.0);\n
warning_element.animate({opacity: 1.0}, {duration: 3000, queue: false});\n
}\n
\n
if(cm.getOption("fullScreen") ||\n
(document.fullScreenElement && document.fullScreenElement !== null) ||\n
document.mozFullScreen || document.webkitIsFullScreen) {\n
var msg_elem_classes = \'maximize_fullscreen_message\';\n
if(error_arr.length || warning_arr.length) {\n
maximize_fullscreen_message = (error_arr.join(\'<br />\') + \'<br />\' +\n
warning_arr.join(\'<br />\'));\n
\n
msg_elem_classes += \' maximize_fullscreen_error_message\';\n
}\n
\n
// Clear previous saving message if any\n
maximizeFullscreenRemoveSaveMessage();\n
\n
msg_elem = $(\'<div class="\' + msg_elem_classes + \'"\' +\n
\'<div>\' + maximize_fullscreen_message + \'</div></div>\');\n
\n
msg_elem.appendTo(cm.getWrapperElement());\n
\n
function animateMessageComplete() {\n
if(!error_arr.length && !warning_arr.length)\n
$(this).remove();\n
else\n
$(this).bind(\'click\', function() { $(this).remove() });\n
}\n
msg_elem.animate({opacity: 1.0}, 1500, animateMessageComplete);\n
}\n
}\n
\n
function errorHandler(data, textStatus) {\n
alert(\'Saving failed: \' + textStatus);\n
}\n
\n
cm.save();\n
var edit_data = $(\'form#main_form\').serialize();\n
edit_data += \'&message_only:int=1\';\n
$.ajax({type: \'POST\',\n
url: \'Base_edit\',\n
data: edit_data,\n
success: successHandler,\n
error: errorHandler});\n
\n
return false;\n
}\n
\n
function switchToFullScreen(cm) {\n
element = $(\'#merge\')[0];\n
$(cm.getWrapperElement()).css(\'height\', \'100%\');\n
if((document.fullScreenElement &&\n
document.fullScreenElement !== null) ||\n
(!document.mozFullScreen && !document.webkitIsFullScreen)) {\n
if (element.requestFullScreen) {\n
element.requestFullScreen();\n
}\n
else if(element.mozRequestFullScreen) {\n
element.mozRequestFullScreen();\n
}\n
else if(element.webkitRequestFullScreen) {\n
element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);\n
}\n
\n
cm.refresh();\n
}\n
}\n
\n
is_maximized = false;\n
function maximize() {\n
document.documentElement.style.overflow = "hidden";\n
$("#merge").addClass(\'maximize\');\n
\n
if(merge_mode_elem) {\n
$("#view").height("100%");\n
\n
cm_merge_height = $(\'.CodeMirror-merge\').height();\n
$(\'.CodeMirror-merge\').height("100%");\n
\n
cm_merge_pane_height = $(\'.CodeMirror-merge-pane\').height();\n
$(\'.CodeMirror-merge-pane\').height("100%");\n
\n
cm_height = merge_mode_elem.edit.getWrapperElement().style.height;\n
merge_mode_elem.edit.getWrapperElement().style.height = "100%";\n
merge_mode_elem.right.orig.getWrapperElement().style.height = "100%";\n
\n
// $("#view").height("100%")\n
\n
merge_mode_elem.edit.refresh();\n
merge_mode_elem.right.orig.refresh();\n
merge_mode_elem.edit.focus();\n
}\n
else {\n
wrap = cm.getWrapperElement();\n
cm_height = wrap.style.height;\n
wrap.style.height = "100%";\n
cm.refresh();\n
cm.focus();\n
}\n
\n
$("body").keyup(\n
function(event) {\n
if(is_maximized && event.keyCode == 27) {\n
event.stopPropagation();\n
event.preventDefault();\n
\n
document.documentElement.style.overflow = "";\n
$("#merge").removeClass(\'maximize\');\n
\n
if(merge_mode_elem) {\n
$(\'.CodeMirror-merge\').height(cm_merge_height);\n
$(\'.CodeMirror-merge-pane\').height(cm_merge_pane_height);\n
$("#view").height("auto");\n
\n
merge_mode_elem.edit.getWrapperElement().style.height = cm_height;\n
merge_mode_elem.right.orig.getWrapperElement().style.height = cm_height;\n
\n
merge_mode_elem.edit.refresh();\n
merge_mode_elem.right.orig.refresh();\n
\n
merge_mode_elem.edit.focus();\n
}\n
else {\n
cm.getWrapperElement().style.height = cm_height;\n
cm.refresh();\n
cm.focus();\n
}\n
\n
is_maximized = false;\n
maximizeFullscreenRemoveSaveMessage();\n
return false;\n
}\n
});\n
\n
$(\'body\').prepend(maximize_mode_message);\n
maximize_mode_message.css(\'opacity\', 1.0);\n
maximize_mode_message.animate({opacity: 0.25}, 1500,\n
function() { $(this).remove(); });\n
\n
is_maximized = true;\n
}\n
\n
// CodeMirror expects a DOM element, not a JQuery Object\n
var cm = CodeMirror.fromTextArea(\n
$(\'#&dtml-field_id;\')[0],\n
{mode: "python",\n
lineNumbers: true,\n
showTrailingSpace: true,\n
matchBrackets: true,\n
viewportMargin: Infinity,\n
extraKeys: {"Ctrl-Space": "autocomplete",\n
"Ctrl-Q": function(cm){cm.foldCode(cm.getCursor());},\n
"Ctrl-S": function(cm) {saveDocument(cm, $.Event(\'click\'))}},\n
foldGutter: true,\n
lineWrapping: true,\n
gutters: ["CodeMirror-linenumbers",\n
"CodeMirror-foldgutter"]});\n
//cm.foldCode(CodeMirror.Pos(8, 0));\n
\n
updateErrorWarningMessageDivWithJump();\n
\n
if(typeof document.cancelFullScreen != \'undefined\' ||\n
(typeof document.mozFullScreenEnabled != \'undefined\' &&\n
document.mozFullScreenEnabled) ||\n
typeof document.webkitCancelFullScreen != \'undefined\') {\n
$(document).bind(\'webkitfullscreenchange mozfullscreenchange fullscreenchange\',\n
maximizeFullscreenRemoveSaveMessage);\n
}\n
\n
function displayLoadSourceCodeMessage() {\n
// TODO: Improve message\n
message = $(\'<span id="maximize_message">Loaded source code</span>\');\n
$(\'body\').prepend(message);\n
message.css(\'opacity\', 1.0);\n
message.animate({opacity: 0.25}, 1500,\n
function() { $(this).remove(); });\n
}\n
\n
function enterMerge(data) {\n
$("#view").show();\n
target = $("#view")[0];\n
target.innerHTML = "";\n
merge_mode_elem = CodeMirror.MergeView(\n
target,\n
{value: cm.getValue(),\n
orig: data,\n
highlightDifferences: true,\n
mode: "python",\n
lineNumbers: true,\n
showTrailingSpace: true,\n
matchBrackets: true,\n
/* viewportMargin: Infinity, */\n
extraKeys: {"Ctrl-Space": "autocomplete"},\n
foldGutter: true,\n
lineWrapping: true,\n
gutters: ["CodeMirror-linenumbers",\n
"CodeMirror-foldgutter"]});\n
\n
$("#merge").keyup(\n
function(event) {\n
if(!is_maximized && event.keyCode == 27) {\n
event.stopPropagation();\n
event.preventDefault();\n
\n
$(\'#history_select_right\').find(\'option[value=""]\').attr("selected", true);\n
leaveMerge();\n
}\n
});\n
\n
$(cm.getWrapperElement()).hide();\n
if(is_maximized)\n
maximize();\n
}\n
\n
function leaveMerge() {\n
$("#view").hide();\n
$(cm.getWrapperElement()).show();\n
cm.refresh();\n
cm.setValue(merge_mode_elem.edit.getValue());\n
cm.save();\n
cm.focus();\n
\n
target = $("#view")[0];\n
target.innerHTML = "";\n
\n
merge_mode_elem = null;\n
\n
if(is_maximized)\n
maximize();\n
}\n
\n
function generateHistorySelectElement(data) {\n
container_elem = $(\'<p style="margin: 0; padding: 0;"></p>\');\n
for(var i = 0; i < 2; i++)\n
{\n
var is_right = (i == 1);\n
if(is_right)\n
attrs = \'id="history_select_right" style="float: right"\'\n
else\n
attrs = \'id="history_select_left" style="float: left"\'\n
\n
select_revision_element = $(\n
"<select class=\'editor_action_button\' " + attrs + "></select>");\n
\n
if(is_right)\n
select_revision_element.append($("<option value=\'\'></option>"));\n
\n
$.each(\n
data,\n
function(j, d) {\n
select_revision_element.append(\n
$("<option value=\'" + d[\'key\'] + "\'>" + j + ": " +\n
new Date(d[\'time\'] * 1000).toString() + " (" + d[\'user_name\'] + ")" +\n
"</option>"));\n
});\n
\n
function selectHistoryTextContent(event) {\n
function loadTextContent(data) {\n
cm.setValue(data);\n
cm.save();\n
\n
if(merge_mode_elem) {\n
merge_mode_elem.edit.setValue(data);\n
merge_mode_elem.edit.refresh();\n
merge_mode_elem.right.orig.refresh();\n
}\n
\n
displayLoadSourceCodeMessage();\n
}\n
\n
// TODO: failure\n
$.ajax({type: \'GET\',\n
async: true,\n
dataType: \'text\',\n
data: {key: this.value},\n
url: \'getTextContentHistory\',\n
success: loadTextContent});\n
}\n
\n
function selectHistoryTextContentMerge(event) {\n
function loadTextContent(data) {\n
if(merge_mode_elem) {\n
merge_mode_elem.right.orig.setValue(data);\n
merge_mode_elem.edit.refresh();\n
merge_mode_elem.right.orig.refresh();\n
}\n
else\n
enterMerge(data);\n
\n
displayLoadSourceCodeMessage();\n
}\n
\n
if(!this.value)\n
leaveMerge();\n
else\n
// TODO: failure\n
$.ajax({type: \'GET\',\n
async: true,\n
dataType: \'text\',\n
data: {key: this.value},\n
url: \'getTextContentHistory\',\n
success: loadTextContent});\n
}\n
\n
if(!is_right)\n
select_revision_element.bind(\'change\', selectHistoryTextContent);\n
else\n
select_revision_element.bind(\'change\', selectHistoryTextContentMerge);\n
\n
select_revision_element.appendTo(container_elem);\n
}\n
\n
container_elem.append($(\'<div style="clear: both;"></div>\'));\n
container_elem.prependTo($(\'#merge\'));\n
}\n
\n
$.ajax({type: \'GET\',\n
async: true,\n
dataType: \'json\',\n
url: \'Component_getTextContentHistoryRevisionDictListAsJSON\',\n
success: generateHistorySelectElement});\n
</script>\n
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<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_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<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>_body</string> </key>
<value> <string>import json\n
return json.dumps(context.getTextContentHistoryRevisionDictList())\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Component_getTextContentHistoryRevisionDictListAsJSON</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -75,7 +75,8 @@ class EditorWidget(Widget.TextAreaWidget):
('Xinha Editor', 'xinha'),
('SVG Editor', 'svg_editor'),
('Spreadsheet Editor', 'spreadsheet_editor'),
('Ace Editor', 'ace')])
('Ace Editor', 'ace'),
('CodeMirror', 'codemirror')])
def render(self, field, key, value, REQUEST, render_prefix=None):
"""
......@@ -118,6 +119,14 @@ class EditorWidget(Widget.TextAreaWidget):
return ace_editor_support.pt_render(extra_context={'field': field,
'content': value,
'id': key})
elif text_editor == 'codemirror':
code_mirror_support = getattr(here, 'code_mirror_support', None)
if code_mirror_support is not None:
site_root = here.getWebSiteValue() or here.getPortalObject()
return code_mirror_support(field=field,
content=value,
field_id=key,
portal_url=site_root.absolute_url())
elif text_editor != 'text_area':
return here.fckeditor_wysiwyg_support.pt_render(
extra_context= {
......
......@@ -355,3 +355,47 @@ class ComponentMixin(PropertyRecordableMixin, Base):
new_component.validate()
return new_component
security.declareProtected(Permissions.ModifyPortalContent,
'getTextContentHistoryRevisionDictList')
def getTextContentHistoryRevisionDictList(self, limit=100):
"""
TODO
"""
history_dict_list = self._p_jar.db().history(self._p_oid, size=limit)
if history_dict_list is None:
# Storage doesn't support history
return ()
from struct import unpack
from OFS.History import historicalRevision
previous_text_content = None
result = []
for history_dict in history_dict_list:
text_content = historicalRevision(self, history_dict['tid']).getTextContent()
if text_content and text_content != previous_text_content:
history_dict['time'] = history_dict['time']
history_dict['user_name'] = history_dict['user_name'].strip()
history_dict['key'] = '.'.join(map(str, unpack(">HHHH", history_dict['tid'])))
del history_dict['tid']
del history_dict['size']
result.append(history_dict)
previous_text_content = text_content
return result
security.declareProtected(Permissions.ModifyPortalContent,
'getTextContentHistory')
def getTextContentHistory(self, key):
"""
TODO
"""
from struct import pack
from OFS.History import historicalRevision
serial = apply(pack, ('>HHHH',) + tuple(map(int, key.split('.'))))
rev = historicalRevision(self, serial)
return rev.getTextContent()
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!