codemirror.js
850 lines
| 26.2 KiB
| application/javascript
|
JavascriptLexer
r2487 | // # Copyright (C) 2010-2018 RhodeCode GmbH | |||
r1 | // # | |||
// # This program is free software: you can redistribute it and/or modify | ||||
// # it under the terms of the GNU Affero General Public License, version 3 | ||||
// # (only), as published by the Free Software Foundation. | ||||
// # | ||||
// # This program is distributed in the hope that it will be useful, | ||||
// # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
// # GNU General Public License for more details. | ||||
// # | ||||
// # You should have received a copy of the GNU Affero General Public License | ||||
// # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
// # | ||||
// # This program is dual-licensed. If you wish to learn more about the | ||||
// # RhodeCode Enterprise Edition, including its added features, Support services, | ||||
// # and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
/** | ||||
* Code Mirror | ||||
*/ | ||||
// global code-mirror logger;, to enable run | ||||
// Logger.get('CodeMirror').setLevel(Logger.DEBUG) | ||||
cmLog = Logger.get('CodeMirror'); | ||||
cmLog.setLevel(Logger.OFF); | ||||
//global cache for inline forms | ||||
var userHintsCache = {}; | ||||
r874 | // global timer, used to cancel async loading | |||
var CodeMirrorLoadUserHintTimer; | ||||
var escapeRegExChars = function(value) { | ||||
return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); | ||||
}; | ||||
/** | ||||
* Load hints from external source returns an array of objects in a format | ||||
* that hinting lib requires | ||||
* @returns {Array} | ||||
*/ | ||||
var CodeMirrorLoadUserHints = function(query, triggerHints) { | ||||
cmLog.debug('Loading mentions users via AJAX'); | ||||
var _users = []; | ||||
$.ajax({ | ||||
type: 'GET', | ||||
data: {query: query}, | ||||
url: pyroutes.url('user_autocomplete_data'), | ||||
headers: {'X-PARTIAL-XHR': true}, | ||||
async: true | ||||
}) | ||||
.done(function(data) { | ||||
var tmpl = '<img class="gravatar" src="{0}"/>{1}'; | ||||
$.each(data.suggestions, function(i) { | ||||
var userObj = data.suggestions[i]; | ||||
if (userObj.username !== "default") { | ||||
_users.push({ | ||||
text: userObj.username + " ", | ||||
org_text: userObj.username, | ||||
displayText: userObj.value_display, // search that field | ||||
// internal caches | ||||
_icon_link: userObj.icon_link, | ||||
_text: userObj.value_display, | ||||
render: function(elt, data, completion) { | ||||
var el = document.createElement('div'); | ||||
el.className = "CodeMirror-hint-entry"; | ||||
el.innerHTML = tmpl.format( | ||||
completion._icon_link, completion._text); | ||||
elt.appendChild(el); | ||||
} | ||||
}); | ||||
} | ||||
}); | ||||
cmLog.debug('Mention users loaded'); | ||||
// set to global cache | ||||
userHintsCache[query] = _users; | ||||
triggerHints(userHintsCache[query]); | ||||
}) | ||||
.fail(function(data, textStatus, xhr) { | ||||
r1971 | alert("error processing request. \n" + | |||
"Error code {0} ({1}).".format(data.status, data.statusText)); | ||||
r874 | }); | |||
}; | ||||
/** | ||||
* filters the results based on the current context | ||||
* @param users | ||||
* @param context | ||||
* @returns {Array} | ||||
*/ | ||||
var CodeMirrorFilterUsers = function(users, context) { | ||||
var MAX_LIMIT = 10; | ||||
var filtered_users = []; | ||||
var curWord = context.string; | ||||
cmLog.debug('Filtering users based on query:', curWord); | ||||
$.each(users, function(i) { | ||||
var match = users[i]; | ||||
var searchText = match.displayText; | ||||
if (!curWord || | ||||
searchText.toLowerCase().lastIndexOf(curWord) !== -1) { | ||||
// reset state | ||||
match._text = match.displayText; | ||||
if (curWord) { | ||||
// do highlighting | ||||
var pattern = '(' + escapeRegExChars(curWord) + ')'; | ||||
match._text = searchText.replace( | ||||
new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); | ||||
} | ||||
filtered_users.push(match); | ||||
} | ||||
// to not return to many results, use limit of filtered results | ||||
if (filtered_users.length > MAX_LIMIT) { | ||||
return false; | ||||
} | ||||
}); | ||||
return filtered_users; | ||||
}; | ||||
var CodeMirrorMentionHint = function(editor, callback, options) { | ||||
var cur = editor.getCursor(); | ||||
var curLine = editor.getLine(cur.line).slice(0, cur.ch); | ||||
// match on @ +1char | ||||
var tokenMatch = new RegExp( | ||||
'(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine); | ||||
var tokenStr = ''; | ||||
if (tokenMatch !== null && tokenMatch.length > 0){ | ||||
tokenStr = tokenMatch[0].strip(); | ||||
} else { | ||||
// skip if we didn't match our token | ||||
return; | ||||
} | ||||
var context = { | ||||
start: (cur.ch - tokenStr.length) + 1, | ||||
end: cur.ch, | ||||
string: tokenStr.slice(1), | ||||
type: null | ||||
}; | ||||
// case when we put the @sign in fron of a string, | ||||
// eg <@ we put it here>sometext then we need to prepend to text | ||||
if (context.end > cur.ch) { | ||||
context.start = context.start + 1; // we add to the @ sign | ||||
context.end = cur.ch; // don't eat front part just append | ||||
context.string = context.string.slice(1, cur.ch - context.start); | ||||
} | ||||
cmLog.debug('Mention context', context); | ||||
var triggerHints = function(userHints){ | ||||
return callback({ | ||||
list: CodeMirrorFilterUsers(userHints, context), | ||||
from: CodeMirror.Pos(cur.line, context.start), | ||||
to: CodeMirror.Pos(cur.line, context.end) | ||||
}); | ||||
}; | ||||
var queryBasedHintsCache = undefined; | ||||
// if we have something in the cache, try to fetch the query based cache | ||||
if (userHintsCache !== {}){ | ||||
queryBasedHintsCache = userHintsCache[context.string]; | ||||
} | ||||
if (queryBasedHintsCache !== undefined) { | ||||
cmLog.debug('Users loaded from cache'); | ||||
triggerHints(queryBasedHintsCache); | ||||
} else { | ||||
// this takes care for async loading, and then displaying results | ||||
// and also propagates the userHintsCache | ||||
window.clearTimeout(CodeMirrorLoadUserHintTimer); | ||||
CodeMirrorLoadUserHintTimer = setTimeout(function() { | ||||
CodeMirrorLoadUserHints(context.string, triggerHints); | ||||
}, 300); | ||||
} | ||||
}; | ||||
var CodeMirrorCompleteAfter = function(cm, pred) { | ||||
var options = { | ||||
completeSingle: false, | ||||
async: true, | ||||
closeOnUnfocus: true | ||||
}; | ||||
var cur = cm.getCursor(); | ||||
setTimeout(function() { | ||||
if (!cm.state.completionActive) { | ||||
cmLog.debug('Trigger mentions hinting'); | ||||
CodeMirror.showHint(cm, CodeMirror.hint.mentions, options); | ||||
} | ||||
}, 100); | ||||
// tell CodeMirror we didn't handle the key | ||||
// trick to trigger on a char but still complete it | ||||
return CodeMirror.Pass; | ||||
}; | ||||
r1 | ||||
var initCodeMirror = function(textAreadId, resetUrl, focus, options) { | ||||
var ta = $('#' + textAreadId).get(0); | ||||
if (focus === undefined) { | ||||
focus = true; | ||||
} | ||||
// default options | ||||
var codeMirrorOptions = { | ||||
mode: "null", | ||||
lineNumbers: true, | ||||
indentUnit: 4, | ||||
autofocus: focus | ||||
}; | ||||
if (options !== undefined) { | ||||
// extend with custom options | ||||
codeMirrorOptions = $.extend(true, codeMirrorOptions, options); | ||||
} | ||||
var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions); | ||||
$('#reset').on('click', function(e) { | ||||
window.location = resetUrl; | ||||
}); | ||||
return myCodeMirror; | ||||
}; | ||||
r2816 | ||||
var initMarkupCodeMirror = function(textAreadId, focus, options) { | ||||
var initialHeight = 100; | ||||
var ta = $(textAreadId).get(0); | ||||
if (focus === undefined) { | ||||
focus = true; | ||||
} | ||||
// default options | ||||
var codeMirrorOptions = { | ||||
lineNumbers: false, | ||||
indentUnit: 4, | ||||
viewportMargin: 30, | ||||
// this is a trick to trigger some logic behind codemirror placeholder | ||||
// it influences styling and behaviour. | ||||
placeholder: " ", | ||||
lineWrapping: true, | ||||
autofocus: focus | ||||
}; | ||||
if (options !== undefined) { | ||||
// extend with custom options | ||||
codeMirrorOptions = $.extend(true, codeMirrorOptions, options); | ||||
} | ||||
var cm = CodeMirror.fromTextArea(ta, codeMirrorOptions); | ||||
cm.setSize(null, initialHeight); | ||||
cm.setOption("mode", DEFAULT_RENDERER); | ||||
CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode | ||||
cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER); | ||||
// start listening on changes to make auto-expanded editor | ||||
cm.on("change", function(instance, changeObj) { | ||||
var height = initialHeight; | ||||
var lines = instance.lineCount(); | ||||
if ( lines > 6 && lines < 20) { | ||||
height = "auto"; | ||||
} | ||||
else if (lines >= 20){ | ||||
zheight = 20*15; | ||||
} | ||||
instance.setSize(null, height); | ||||
// detect if the change was trigger by auto desc, or user input | ||||
var changeOrigin = changeObj.origin; | ||||
if (changeOrigin === "setValue") { | ||||
cmLog.debug('Change triggered by setValue'); | ||||
} | ||||
else { | ||||
cmLog.debug('user triggered change !'); | ||||
// set special marker to indicate user has created an input. | ||||
instance._userDefinedValue = true; | ||||
} | ||||
}); | ||||
return cm; | ||||
}; | ||||
r1362 | var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){ | |||
r1 | var initialHeight = 100; | |||
if (typeof userHintsCache === "undefined") { | ||||
userHintsCache = {}; | ||||
cmLog.debug('Init empty cache for mentions'); | ||||
} | ||||
if (!$(textAreaId).get(0)) { | ||||
cmLog.debug('Element for textarea not found', textAreaId); | ||||
return; | ||||
} | ||||
/** | ||||
* Filter action based on typed in text | ||||
* @param actions | ||||
* @param context | ||||
* @returns {Array} | ||||
*/ | ||||
var filterActions = function(actions, context){ | ||||
r1361 | ||||
r1 | var MAX_LIMIT = 10; | |||
r1361 | var filtered_actions = []; | |||
r1 | var curWord = context.string; | |||
cmLog.debug('Filtering actions based on query:', curWord); | ||||
$.each(actions, function(i) { | ||||
var match = actions[i]; | ||||
r1361 | var searchText = match.searchText; | |||
r1 | ||||
if (!curWord || | ||||
searchText.toLowerCase().lastIndexOf(curWord) !== -1) { | ||||
// reset state | ||||
match._text = match.displayText; | ||||
if (curWord) { | ||||
// do highlighting | ||||
var pattern = '(' + escapeRegExChars(curWord) + ')'; | ||||
match._text = searchText.replace( | ||||
new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); | ||||
} | ||||
filtered_actions.push(match); | ||||
} | ||||
// to not return to many results, use limit of filtered results | ||||
if (filtered_actions.length > MAX_LIMIT) { | ||||
return false; | ||||
} | ||||
}); | ||||
r1361 | ||||
r1 | return filtered_actions; | |||
}; | ||||
var submitForm = function(cm, pred) { | ||||
$(cm.display.input.textarea.form).submit(); | ||||
return CodeMirror.Pass; | ||||
}; | ||||
r1361 | var completeActions = function(actions){ | |||
r1362 | var registeredActions = []; | |||
var allActions = [ | ||||
{ | ||||
text: "approve", | ||||
searchText: "status approved", | ||||
displayText: _gettext('Set status to Approved'), | ||||
hint: function(CodeMirror, data, completion) { | ||||
CodeMirror.replaceRange("", completion.from || data.from, | ||||
completion.to || data.to, "complete"); | ||||
$(CommentForm.statusChange).select2("val", 'approved').trigger('change'); | ||||
}, | ||||
render: function(elt, data, completion) { | ||||
var el = document.createElement('div'); | ||||
el.className = "flag_status flag_status_comment_box approved pull-left"; | ||||
elt.appendChild(el); | ||||
el = document.createElement('span'); | ||||
el.innerHTML = completion.displayText; | ||||
elt.appendChild(el); | ||||
} | ||||
}, | ||||
{ | ||||
text: "reject", | ||||
searchText: "status rejected", | ||||
displayText: _gettext('Set status to Rejected'), | ||||
hint: function(CodeMirror, data, completion) { | ||||
CodeMirror.replaceRange("", completion.from || data.from, | ||||
completion.to || data.to, "complete"); | ||||
$(CommentForm.statusChange).select2("val", 'rejected').trigger('change'); | ||||
}, | ||||
render: function(elt, data, completion) { | ||||
var el = document.createElement('div'); | ||||
el.className = "flag_status flag_status_comment_box rejected pull-left"; | ||||
elt.appendChild(el); | ||||
el = document.createElement('span'); | ||||
el.innerHTML = completion.displayText; | ||||
elt.appendChild(el); | ||||
} | ||||
}, | ||||
{ | ||||
text: "as_todo", | ||||
searchText: "todo comment", | ||||
displayText: _gettext('TODO comment'), | ||||
hint: function(CodeMirror, data, completion) { | ||||
CodeMirror.replaceRange("", completion.from || data.from, | ||||
completion.to || data.to, "complete"); | ||||
$(CommentForm.commentType).val('todo'); | ||||
}, | ||||
render: function(elt, data, completion) { | ||||
var el = document.createElement('div'); | ||||
el.className = "pull-left"; | ||||
elt.appendChild(el); | ||||
el = document.createElement('span'); | ||||
el.innerHTML = completion.displayText; | ||||
elt.appendChild(el); | ||||
} | ||||
}, | ||||
{ | ||||
text: "as_note", | ||||
searchText: "note comment", | ||||
displayText: _gettext('Note Comment'), | ||||
hint: function(CodeMirror, data, completion) { | ||||
CodeMirror.replaceRange("", completion.from || data.from, | ||||
completion.to || data.to, "complete"); | ||||
$(CommentForm.commentType).val('note'); | ||||
}, | ||||
render: function(elt, data, completion) { | ||||
var el = document.createElement('div'); | ||||
el.className = "pull-left"; | ||||
elt.appendChild(el); | ||||
el = document.createElement('span'); | ||||
el.innerHTML = completion.displayText; | ||||
elt.appendChild(el); | ||||
} | ||||
} | ||||
]; | ||||
$.each(allActions, function(index, value){ | ||||
var actionData = allActions[index]; | ||||
if (actions.indexOf(actionData['text']) != -1) { | ||||
registeredActions.push(actionData); | ||||
} | ||||
}); | ||||
r1361 | return function(cm, pred) { | |||
var cur = cm.getCursor(); | ||||
var options = { | ||||
r1362 | closeOnUnfocus: true, | |||
registeredActions: registeredActions | ||||
r1361 | }; | |||
setTimeout(function() { | ||||
if (!cm.state.completionActive) { | ||||
cmLog.debug('Trigger actions hinting'); | ||||
CodeMirror.showHint(cm, CodeMirror.hint.actions, options); | ||||
} | ||||
}, 100); | ||||
// tell CodeMirror we didn't handle the key | ||||
// trick to trigger on a char but still complete it | ||||
return CodeMirror.Pass; | ||||
r1 | } | |||
}; | ||||
var extraKeys = { | ||||
r874 | "'@'": CodeMirrorCompleteAfter, | |||
r1 | Tab: function(cm) { | |||
// space indent instead of TABS | ||||
var spaces = new Array(cm.getOption("indentUnit") + 1).join(" "); | ||||
cm.replaceSelection(spaces); | ||||
} | ||||
}; | ||||
// submit form on Meta-Enter | ||||
if (OSType === "mac") { | ||||
extraKeys["Cmd-Enter"] = submitForm; | ||||
} | ||||
else { | ||||
extraKeys["Ctrl-Enter"] = submitForm; | ||||
} | ||||
if (triggerActions) { | ||||
r1361 | // register triggerActions for this instance | |||
extraKeys["'/'"] = completeActions(triggerActions); | ||||
r1 | } | |||
var cm = CodeMirror.fromTextArea($(textAreaId).get(0), { | ||||
lineNumbers: false, | ||||
indentUnit: 4, | ||||
viewportMargin: 30, | ||||
// this is a trick to trigger some logic behind codemirror placeholder | ||||
// it influences styling and behaviour. | ||||
placeholder: " ", | ||||
extraKeys: extraKeys, | ||||
lineWrapping: true | ||||
}); | ||||
cm.setSize(null, initialHeight); | ||||
cm.setOption("mode", DEFAULT_RENDERER); | ||||
CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode | ||||
cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER); | ||||
// start listening on changes to make auto-expanded editor | ||||
cm.on("change", function(self) { | ||||
var height = initialHeight; | ||||
var lines = self.lineCount(); | ||||
if ( lines > 6 && lines < 20) { | ||||
height = "auto"; | ||||
} | ||||
else if (lines >= 20){ | ||||
zheight = 20*15; | ||||
} | ||||
self.setSize(null, height); | ||||
}); | ||||
var actionHint = function(editor, options) { | ||||
r1362 | ||||
r1 | var cur = editor.getCursor(); | |||
var curLine = editor.getLine(cur.line).slice(0, cur.ch); | ||||
r1361 | // match only on /+1 character minimum | |||
var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine); | ||||
r1 | ||||
var tokenStr = ''; | ||||
if (tokenMatch !== null && tokenMatch.length > 0){ | ||||
r1361 | tokenStr = tokenMatch[2].strip(); | |||
r1 | } | |||
var context = { | ||||
r1361 | start: (cur.ch - tokenStr.length) - 1, | |||
r1 | end: cur.ch, | |||
string: tokenStr, | ||||
type: null | ||||
}; | ||||
return { | ||||
r1362 | list: filterActions(options.registeredActions, context), | |||
r1 | from: CodeMirror.Pos(cur.line, context.start), | |||
to: CodeMirror.Pos(cur.line, context.end) | ||||
}; | ||||
r1361 | ||||
r1 | }; | |||
r874 | CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint); | |||
r1 | CodeMirror.registerHelper("hint", "actions", actionHint); | |||
return cm; | ||||
}; | ||||
var setCodeMirrorMode = function(codeMirrorInstance, mode) { | ||||
CodeMirror.autoLoadMode(codeMirrorInstance, mode); | ||||
codeMirrorInstance.setOption("mode", mode); | ||||
}; | ||||
var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) { | ||||
codeMirrorInstance.setOption("lineWrapping", line_wrap); | ||||
}; | ||||
var setCodeMirrorModeFromSelect = function( | ||||
targetSelect, targetFileInput, codeMirrorInstance, callback){ | ||||
$(targetSelect).on('change', function(e) { | ||||
cmLog.debug('codemirror select2 mode change event !'); | ||||
var selected = e.currentTarget; | ||||
var node = selected.options[selected.selectedIndex]; | ||||
var mimetype = node.value; | ||||
cmLog.debug('picked mimetype', mimetype); | ||||
var new_mode = $(node).attr('mode'); | ||||
setCodeMirrorMode(codeMirrorInstance, new_mode); | ||||
cmLog.debug('set new mode', new_mode); | ||||
//propose filename from picked mode | ||||
cmLog.debug('setting mimetype', mimetype); | ||||
var proposed_ext = getExtFromMimeType(mimetype); | ||||
cmLog.debug('file input', $(targetFileInput).val()); | ||||
var file_data = getFilenameAndExt($(targetFileInput).val()); | ||||
var filename = file_data.filename || 'filename1'; | ||||
$(targetFileInput).val(filename + proposed_ext); | ||||
cmLog.debug('proposed file', filename + proposed_ext); | ||||
if (typeof(callback) === 'function') { | ||||
try { | ||||
cmLog.debug('running callback', callback); | ||||
callback(filename, mimetype, new_mode); | ||||
} catch (err) { | ||||
console.log('failed to run callback', callback, err); | ||||
} | ||||
} | ||||
cmLog.debug('finish iteration...'); | ||||
}); | ||||
}; | ||||
var setCodeMirrorModeFromInput = function( | ||||
targetSelect, targetFileInput, codeMirrorInstance, callback) { | ||||
// on type the new filename set mode | ||||
$(targetFileInput).on('keyup', function(e) { | ||||
var file_data = getFilenameAndExt(this.value); | ||||
if (file_data.ext === null) { | ||||
return; | ||||
} | ||||
var mimetypes = getMimeTypeFromExt(file_data.ext, true); | ||||
cmLog.debug('mimetype from file', file_data, mimetypes); | ||||
var detected_mode; | ||||
var detected_option; | ||||
for (var i in mimetypes) { | ||||
var mt = mimetypes[i]; | ||||
if (!detected_mode) { | ||||
detected_mode = detectCodeMirrorMode(this.value, mt); | ||||
} | ||||
if (!detected_option) { | ||||
cmLog.debug('#mimetype option[value="{0}"]'.format(mt)); | ||||
if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) { | ||||
detected_option = mt; | ||||
} | ||||
} | ||||
} | ||||
cmLog.debug('detected mode', detected_mode); | ||||
cmLog.debug('detected option', detected_option); | ||||
if (detected_mode && detected_option){ | ||||
$(targetSelect).select2("val", detected_option); | ||||
setCodeMirrorMode(codeMirrorInstance, detected_mode); | ||||
if(typeof(callback) === 'function'){ | ||||
try{ | ||||
cmLog.debug('running callback', callback); | ||||
var filename = file_data.filename + "." + file_data.ext; | ||||
callback(filename, detected_option, detected_mode); | ||||
}catch (err){ | ||||
console.log('failed to run callback', callback, err); | ||||
} | ||||
} | ||||
} | ||||
}); | ||||
}; | ||||
var fillCodeMirrorOptions = function(targetSelect) { | ||||
//inject new modes, based on codeMirrors modeInfo object | ||||
var modes_select = $(targetSelect); | ||||
for (var i = 0; i < CodeMirror.modeInfo.length; i++) { | ||||
var m = CodeMirror.modeInfo[i]; | ||||
var opt = new Option(m.name, m.mime); | ||||
$(opt).attr('mode', m.mode); | ||||
modes_select.append(opt); | ||||
} | ||||
}; | ||||
var CodeMirrorPreviewEnable = function(edit_mode) { | ||||
// in case it a preview enabled mode enable the button | ||||
if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) { | ||||
$('#render_preview').removeClass('hidden'); | ||||
} | ||||
else { | ||||
if (!$('#render_preview').hasClass('hidden')) { | ||||
$('#render_preview').addClass('hidden'); | ||||
} | ||||
} | ||||
}; | ||||
r2816 | ||||
/* markup form */ | ||||
(function(mod) { | ||||
if (typeof exports == "object" && typeof module == "object") { | ||||
// CommonJS | ||||
module.exports = mod(); | ||||
} | ||||
else { | ||||
// Plain browser env | ||||
(this || window).MarkupForm = mod(); | ||||
} | ||||
})(function() { | ||||
"use strict"; | ||||
function MarkupForm(textareaId) { | ||||
if (!(this instanceof MarkupForm)) { | ||||
return new MarkupForm(textareaId); | ||||
} | ||||
// bind the element instance to our Form | ||||
$('#' + textareaId).get(0).MarkupForm = this; | ||||
this.withSelectorId = function(selector) { | ||||
var selectorId = textareaId; | ||||
return selector + '_' + selectorId; | ||||
}; | ||||
this.previewButton = this.withSelectorId('#preview-btn'); | ||||
this.previewContainer = this.withSelectorId('#preview-container'); | ||||
this.previewBoxSelector = this.withSelectorId('#preview-box'); | ||||
this.editButton = this.withSelectorId('#edit-btn'); | ||||
this.editContainer = this.withSelectorId('#edit-container'); | ||||
this.cmBox = textareaId; | ||||
this.cm = initMarkupCodeMirror('#' + textareaId); | ||||
this.previewUrl = pyroutes.url('markup_preview'); | ||||
// FUNCTIONS and helpers | ||||
var self = this; | ||||
this.getCmInstance = function(){ | ||||
return this.cm | ||||
}; | ||||
this.setPlaceholder = function(placeholder) { | ||||
var cm = this.getCmInstance(); | ||||
if (cm){ | ||||
cm.setOption('placeholder', placeholder); | ||||
} | ||||
}; | ||||
this.initStatusChangeSelector = function(){ | ||||
var formatChangeStatus = function(state, escapeMarkup) { | ||||
var originalOption = state.element; | ||||
return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' + | ||||
'<span>' + escapeMarkup(state.text) + '</span>'; | ||||
}; | ||||
var formatResult = function(result, container, query, escapeMarkup) { | ||||
return formatChangeStatus(result, escapeMarkup); | ||||
}; | ||||
var formatSelection = function(data, container, escapeMarkup) { | ||||
return formatChangeStatus(data, escapeMarkup); | ||||
}; | ||||
$(this.submitForm).find(this.statusChange).select2({ | ||||
placeholder: _gettext('Status Review'), | ||||
formatResult: formatResult, | ||||
formatSelection: formatSelection, | ||||
containerCssClass: "drop-menu status_box_menu", | ||||
dropdownCssClass: "drop-menu-dropdown", | ||||
dropdownAutoWidth: true, | ||||
minimumResultsForSearch: -1 | ||||
}); | ||||
$(this.submitForm).find(this.statusChange).on('change', function() { | ||||
var status = self.getCommentStatus(); | ||||
if (status && !self.isInline()) { | ||||
$(self.submitButton).prop('disabled', false); | ||||
} | ||||
var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status); | ||||
self.setPlaceholder(placeholderText) | ||||
}) | ||||
}; | ||||
// reset the text area into it's original state | ||||
this.resetMarkupFormState = function(content) { | ||||
content = content || ''; | ||||
$(this.editContainer).show(); | ||||
$(this.editButton).parent().addClass('active'); | ||||
$(this.previewContainer).hide(); | ||||
$(this.previewButton).parent().removeClass('active'); | ||||
this.setActionButtonsDisabled(true); | ||||
self.cm.setValue(content); | ||||
self.cm.setOption("readOnly", false); | ||||
}; | ||||
this.previewSuccessCallback = function(o) { | ||||
$(self.previewBoxSelector).html(o); | ||||
$(self.previewBoxSelector).removeClass('unloaded'); | ||||
// swap buttons, making preview active | ||||
$(self.previewButton).parent().addClass('active'); | ||||
$(self.editButton).parent().removeClass('active'); | ||||
// unlock buttons | ||||
self.setActionButtonsDisabled(false); | ||||
}; | ||||
this.setActionButtonsDisabled = function(state) { | ||||
$(this.editButton).prop('disabled', state); | ||||
$(this.previewButton).prop('disabled', state); | ||||
}; | ||||
// lock preview/edit/submit buttons on load, but exclude cancel button | ||||
var excludeCancelBtn = true; | ||||
this.setActionButtonsDisabled(true); | ||||
// anonymous users don't have access to initialized CM instance | ||||
if (this.cm !== undefined){ | ||||
this.cm.on('change', function(cMirror) { | ||||
if (cMirror.getValue() === "") { | ||||
self.setActionButtonsDisabled(true) | ||||
} else { | ||||
self.setActionButtonsDisabled(false) | ||||
} | ||||
}); | ||||
} | ||||
$(this.editButton).on('click', function(e) { | ||||
e.preventDefault(); | ||||
$(self.previewButton).parent().removeClass('active'); | ||||
$(self.previewContainer).hide(); | ||||
$(self.editButton).parent().addClass('active'); | ||||
$(self.editContainer).show(); | ||||
}); | ||||
$(this.previewButton).on('click', function(e) { | ||||
e.preventDefault(); | ||||
var text = self.cm.getValue(); | ||||
if (text === "") { | ||||
return; | ||||
} | ||||
var postData = { | ||||
'text': text, | ||||
'renderer': templateContext.visual.default_renderer, | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
// lock ALL buttons on preview | ||||
self.setActionButtonsDisabled(true); | ||||
$(self.previewBoxSelector).addClass('unloaded'); | ||||
$(self.previewBoxSelector).html(_gettext('Loading ...')); | ||||
$(self.editContainer).hide(); | ||||
$(self.previewContainer).show(); | ||||
// by default we reset state of comment preserving the text | ||||
var previewFailCallback = function(data){ | ||||
alert( | ||||
"Error while submitting preview.\n" + | ||||
"Error code {0} ({1}).".format(data.status, data.statusText) | ||||
); | ||||
self.resetMarkupFormState(text) | ||||
}; | ||||
_submitAjaxPOST( | ||||
self.previewUrl, postData, self.previewSuccessCallback, | ||||
previewFailCallback); | ||||
$(self.previewButton).parent().addClass('active'); | ||||
$(self.editButton).parent().removeClass('active'); | ||||
}); | ||||
} | ||||
return MarkupForm; | ||||
}); | ||||