/*
* Copyright 2025 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Field that displays a code editor.
* Requires CodeMirror (version 3.14) to be loaded.
* See http://codemirror.net/ to have documentation on it.
*/
Ext.define('Ametys.form.field.CodeAdvanced', {
extend: 'Ametys.form.AbstractField',
alias: ['widget.codeadvanced'],
_editorInitialized: 0,
layout: 'fit',
border: true, // The border should be on the underlying component but the color would be wrong... let's hope noboy needs a label
/**
* @cfg {String} [mode="html"] The CodeMirror mode
*/
/**
* @property {String} The CodeMirror mode. See #cfg-mode
*/
_mode: null,
constructor: function(config)
{
config.cls = Ext.Array.from(config.cls);
config.cls.push('ametys-form-field-code-advanced');
config.items = [
{
xtype: 'component',
name: 'code'
}
];
config._mode = config.mode || 'html';
this.callParent(arguments);
this.on('resize', this._onResize, this);
this.on('initialize', this._init, this);
this.addStateEvents('change');
},
afterComponentLayout: function(width, height, oldWidth, oldWeight)
{
this.callParent(arguments);
// Creates the tinymce editor
if (this._editorInitialized == 0)
{
this._editorInitialized = 1;
this._createEditor();
}
},
_createEditor: function()
{
var me = this;
// Check if Monaco is already loaded globally
if (window.monaco)
{
me._createEditor2();
}
// Load Monaco's AMD loader dynamically to avoid conflicts with CodeMirror
else if (!window.monacoRequire)
{
Ametys.loadScript(Ametys.getPluginResourcesPrefix("monaco-editor") + '/min/vs/loader.js', function () {
// Store Monaco's require in a separate namespace
window.monacoRequire = window.require;
// Configure and load Monaco
window.monacoRequire.config({
paths: { 'vs': Ametys.getPluginResourcesPrefix("monaco-editor") + '/min/vs' }
});
window.monacoRequire(['vs/editor/editor.main'], function() {
// Disable some keybindings that conflict with browser shortcuts
monaco.editor.addKeybindingRules([
/* NOT WORKING {
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.Shift | monaco.KeyCode.KeyR,
command: null
},*/
{
keybinding: monaco.KeyCode.F12,
command: null
}
]);
// validation settings
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false, // Activate semantic validation
noSyntaxValidation: false, // Activate syntax validation
});
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false, // Activate semantic validation
noSyntaxValidation: false, // Activate syntax validation
});
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest, // Compile for the last ES version. At this time, monaco supports for ES2020 and java for ES2024
lib: ["es2020"], // Disable "window/dom"
allowNonTsExtensions: true // Allow JS files
});
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest, // Compile for the last ES version. At this time, monaco supports for ES2020 and java for ES2024
lib: ["es2020"], // Disable "window/dom"
allowNonTsExtensions: true // Allow JS files
});
me._createEditor2();
});
}, function() {
Ametys.log.Error("Code", "Failed to load Monaco editor library.");
});
} else {
// Monaco loader already exists, just load the editor
window.monacoRequire(['vs/editor/editor.main'], function() {
me._createEditor2();
});
}
},
/**
* Create the Monaco editor instance
* @private
*/
_createEditor2: function()
{
let me = this;
let v = me.initialConfig.value || '';
this._monaco = monaco.editor.create(this.query("[name=code]")[0].getEl().dom, {
value: v,
// automaticLayout: true,
scrollBeyondLastLine: false,
minimap: { enabled: false },
language: this._mode,
readOnly: this._readOnly,
fixedOverflowWidgets: true, // let popups overflow outside the editor
lineHeight: 17,
fontSize: 12,
insertSpaces: true,
contextmenu: false,
suggest: {
showWords:false, // Disable suggestions based on words present in the document
}
});
this._previousValue = v;
// Add onFocus event listener
this._monaco.onDidFocusEditorText(function() {
me.onFocus();
});
// Add onBlur event listener
this._monaco.onDidBlurEditorText(function() {
me.onBlur();
});
this._monaco.onDidChangeModelContent(Ext.bind(this._onChange, this));
this._editorInitialized = 2;
me.fireEvent('initialize', me);
},
_onResize: function(component, width, height, oldWidth, oldHeight, eOpts)
{
if (this._monaco)
{
this._monaco.layout({ width: width - 2, height: height - 2}); // -2 to account for borders (that extjs ignores)
}
},
/**
* Returns the parameter(s) that would be included in a standard form submit for this field. Typically this will be
* an object with a single name-value pair, the name being this field's {@link #method-getName name} and the value being
* its current stringified value. More advanced field implementations may return more than one name-value pair.
*
* Note that the values returned from this method are not guaranteed to have been successfully {@link #validate validated}.
*
* @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array of
* strings if that particular name has multiple values. It can also return null if there are no parameters to be
* submitted.
*/
getSubmitData: function()
{
var me = this,
data = null;
if ((!me.disabled || me.submitDisabledValue) && me.submitValue && !me.isFileUpload())
{
data = {};
data[me.getName()] = me.getSubmitValue();
}
return data;
},
getSubmitValue: function ()
{
return '' + this.getValue();
},
reset: function()
{
this.originalValue = this.originalValue || '';
this.callParent(arguments);
},
getValue: function()
{
var value;
if (this._monaco)
{
value = this._monaco.getValue();
}
else if (this._futureValue != null)
{
value = this._futureValue;
}
else
{
value = this.initialConfig.value;
}
return value;
},
setValue: function(v)
{
v = v || '';
this.callParent(arguments);
if (this._monaco)
{
this._monaco.setValue(v);
this._previousValue = v;
}
else
{
this._futureValue = v;
}
},
/**
* @private
* {@link #event-initialize} listener
*/
_init: function()
{
if (!Ext.isEmpty(this._futureValue))
{
this.setValue(this._futureValue);
this._futureValue = null;
}
},
/**
* Listens for change in editor
* @param {Object} event The change event
* @private
*/
_onChange: function(event)
{
if (this._localChange)
{
return;
}
let previousValue = this._previousValue;
let currentValue = this._monaco.getValue();
// If single-line, join the different lines and filter out newline characters.
if (this._singleLine)
{
var newtext = currentValue.join('').replace(/\n/g, '');
this._monaco.setValue(newtext);
return;
}
/**
* @event beforechange
* Fires before the content is changed.
* @param {Ametys.form.field.CodeAdvanced} editor the current component
* @param {String} newValue The new value
* @param {String} oldValue The old value
* @param {Object} event The change event
*/
if (this.fireEvent('beforechange', this, currentValue, previousValue, event) === false)
{
// Annuler la modification en restaurant la valeur précédente
this._localChange = true;
// this._monaco.setValue(previousValue);
//let me = this;
if (event.isRedoing)
{
let me = this;
setTimeout(function() {
me._monaco.getModel().undo();
}, 0);
}
else
{
this._monaco.getModel().undo();
}
this._localChange = false;
return;
}
this.fireEvent('change', this, currentValue, previousValue, event);
this._previousValue = currentValue;
},
getState: function()
{
return {
value: Ext.JSON.encode(this.getValue())
};
},
applyState: function(state)
{
if (state && state.value)
{
this.setValue(Ext.JSON.decode(state.value));
}
this.callParent(arguments);
},
onDestroy: function()
{
this.callParent(arguments);
if (this._monaco)
{
this._monaco.dispose();
}
}
});