33 <div class =" sourceHeader" >
44 <strong v-if =" title" >{{ title }}</strong >
55 <div class =" sourceToolbar" ref =" sourceToolbar" >
6- <span class =" group" >
7- <BButton v-if =" editable" @click =" confirmClear" title =" Start from scratch - Clears the current script" >
6+ <select v-if =" languageChooser" class =" language-chooser" :value =" editorLanguage"
7+ @change =" changeLanguage($event.target.value)" title =" Select language" >
8+ <option value =" " >Plain Text</option >
9+ <option value =" javascript" >JavaScript</option >
10+ <option value =" json" >JSON</option >
11+ <option value =" markdown" >Markdown</option >
12+ <option value =" math" >Math</option >
13+ <option value =" python" >Python</option >
14+ <option value =" r" >R</option >
15+ <option value =" yaml" >YAML</option >
16+ </select >
17+ <span class =" group" v-if =" editable" >
18+ <BButton @click =" confirmClear" title =" Start from scratch - Clears the current script" >
819 <i class =" fas fa-file" ></i > <span class =" text" >New</span >
920 </BButton >
21+ <AsyncButton :fn =" upload" fa icon =" fas fa-cloud-upload-alt" title =" Upload a file from your computer - Clears the current script" ><span class =" text" >Upload</span ></AsyncButton >
1022 <slot name =" file-toolbar" ></slot >
1123 </span >
1224 <span class =" group" v-if =" editable" >
3042import Utils from ' ../utils.js' ;
3143import FullscreenButton from ' ./FullscreenButton.vue' ;
3244import ToolbarCompactMixin from ' ./ToolbarCompactMixin.js' ;
45+ import AsyncButton from ' @openeo/vue-components/components/internal/AsyncButton.vue' ;
3346import BButton from ' @openeo/vue-components/components/internal/BButton.vue' ;
3447import { ProcessGraph } from ' @openeo/js-processgraphs' ;
3548
@@ -58,6 +71,7 @@ export default {
5871 name: ' TextEditor' ,
5972 mixins: [ToolbarCompactMixin],
6073 components: {
74+ AsyncButton,
6175 BButton,
6276 FullscreenButton
6377 },
@@ -80,12 +94,28 @@ export default {
8094 },
8195 title: {
8296 type: String
97+ },
98+ languageChooser: {
99+ type: Boolean ,
100+ default: false
83101 }
84102 },
85103 computed: {
86104 ... Utils .mapGetters ([' processes' ]),
87105 languageString () {
88- return typeof this .language === ' string' ? this .language .toLowerCase () : ' ' ;
106+ return typeof this .activeLanguage === ' string' ? this .activeLanguage .toLowerCase () : ' ' ;
107+ },
108+ editorLanguage () {
109+ // Some additional aliases for some languages
110+ switch (this .languageString ) {
111+ case ' processgraph' :
112+ return ' json' ;
113+ case ' cwl' :
114+ case ' eoap-cwl' :
115+ return ' yaml' ;
116+ default :
117+ return this .languageString ;
118+ }
89119 },
90120 editorOptions () {
91121 let options = {
@@ -95,9 +125,13 @@ export default {
95125 matchBrackets: true ,
96126 autoCloseBrackets: true ,
97127 readOnly: ! this .editable ,
98- placeholder: this .placeholder
128+ placeholder: this .placeholder ,
129+ mode: null ,
130+ gutters: [],
131+ lint: false ,
132+ lineWrapping: false
99133 };
100- switch (this .languageString ) {
134+ switch (this .editorLanguage ) {
101135 case ' r' :
102136 options .mode = ' text/x-rsrc' ;
103137 break ;
@@ -115,17 +149,13 @@ export default {
115149 options .mode = ' text/javascript' ;
116150 break ;
117151 case ' json' :
118- case ' processgraph' :
119152 options .mode = ' application/json' ;
120153 options .gutters = [' CodeMirror-lint-markers' ];
121154 options .lint = true ;
122155 break ;
123156 case ' yaml' :
124- case ' yaml' :
125- case ' cwl' :
126157 options .mode = ' text/x-yaml' ;
127158 options .gutters = [' CodeMirror-lint-markers' ];
128- options .lint = true ;
129159 break ;
130160 }
131161 return options;
@@ -137,7 +167,8 @@ export default {
137167 canRedo: false ,
138168 editor: null ,
139169 emitValue: this .value ,
140- element: null
170+ element: null ,
171+ activeLanguage: this .language
141172 }
142173 },
143174 watch: {
@@ -147,11 +178,13 @@ export default {
147178 this .editor .clearHistory ();
148179 }
149180 },
150- editorOptions () {
151- for (var key in this .editorOptions ) {
152- this .editor .setOption (key, this .editorOptions [key]);
181+ language (newLang ) {
182+ this .activeLanguage = newLang;
183+ },
184+ editorOptions (newOptions ) {
185+ for (var key in newOptions) {
186+ this .editor .setOption (key, newOptions[key]);
153187 }
154- this .updateContent ();
155188 }
156189 },
157190 mounted () {
@@ -177,12 +210,125 @@ export default {
177210 this .element = this .$el ;
178211 },
179212 methods: {
213+ async upload () {
214+ // Create temporary file input element to trigger file selection dialog
215+ const fileInput = document .createElement (' input' );
216+ fileInput .type = ' file' ;
217+ fileInput .style .display = ' none' ;
218+ document .body .appendChild (fileInput);
219+ try {
220+ // Click the file input to open the file dialog
221+ fileInput .click ();
222+ // Wait for the user to select a file (or cancel)
223+ const file = await new Promise (resolve => {
224+ fileInput .addEventListener (' change' , () => {
225+ resolve (fileInput .files [0 ] || null );
226+ }, { once: true });
227+ // Handle cancel: when focus returns without a change event
228+ window .addEventListener (' focus' , () => {
229+ setTimeout (() => resolve (null ), 300 );
230+ }, { once: true });
231+ });
232+ if (! file) {
233+ return false ;
234+ }
235+ // Read the file content as text
236+ const content = await new Promise ((resolve , reject ) => {
237+ const reader = new FileReader ();
238+ reader .onload = () => resolve (reader .result );
239+ reader .onerror = () => reject (reader .error );
240+ reader .readAsText (file);
241+ });
242+ if (! this .confirmClear ()) {
243+ return false ;
244+ }
245+ this .insert (content);
246+ if (this .languageChooser ) {
247+ const lang = this .detectLanguage (file);
248+ if (lang !== null ) {
249+ this .activeLanguage = lang;
250+ this .$emit (' update:language' , lang);
251+ }
252+ }
253+ this .commit ();
254+ return true ;
255+ } catch (error) {
256+ Utils .exception (this , error, " Upload failed" );
257+ return false ;
258+ } finally {
259+ // Clean up the temporary file input element
260+ document .body .removeChild (fileInput);
261+ }
262+ },
263+ detectLanguage (file ) {
264+ // Try to detect language from MIME type
265+ if (typeof file .type === ' string' && file .type .includes (' /' )) {
266+ switch (file .type .toLowerCase ().replace (/ ;. * $ / , ' ' ).trim ()) {
267+ case ' application/json' :
268+ case ' application/geo+json' :
269+ case ' text/json' :
270+ return ' json' ;
271+ case ' application/javascript' :
272+ case ' text/javascript' :
273+ return ' javascript' ;
274+ case ' text/markdown' :
275+ return ' markdown' ;
276+ case ' text/x-python' :
277+ case ' application/x-python' :
278+ return ' python' ;
279+ case ' application/x-yaml' :
280+ case ' text/yaml' :
281+ case ' text/x-yaml' :
282+ return ' yaml' ;
283+ case ' text/plain' :
284+ break ; // fall through to extension detection
285+ default :
286+ return null ;
287+ }
288+ }
289+ // Fallback: detect by file extension
290+ if (typeof file .name === ' string' ) {
291+ const ext = file .name .split (' .' ).pop ().trim ().toLowerCase ();
292+ switch (ext) {
293+ case ' json' :
294+ case ' geojson' :
295+ return ' json' ;
296+ case ' js' :
297+ case ' mjs' :
298+ case ' cjs' :
299+ return ' javascript' ;
300+ case ' md' :
301+ case ' markdown' :
302+ return ' markdown' ;
303+ case ' py' :
304+ return ' python' ;
305+ case ' r' :
306+ return ' r' ;
307+ case ' yml' :
308+ case ' yaml' :
309+ case ' cwl' :
310+ return ' yaml' ;
311+ }
312+ }
313+ return null ;
314+ },
315+ changeLanguage (lang ) {
316+ this .activeLanguage = lang;
317+ this .$emit (' update:language' , lang);
318+ this .$nextTick (() => {
319+ this .editor .refresh ();
320+ });
321+ },
180322 confirmClear () {
181- var confirmed = confirm (" Do you really want to clear the existing code?" );
323+ if (this .editor .getValue ().trim () === " " ) {
324+ return true ;
325+ }
326+ const confirmed = confirm (" Do you really want to clear the existing code?" );
182327 if (confirmed) {
183328 this .insert (" " );
184329 this .emit (null );
185330 }
331+ return confirmed;
186332 },
187333 updateState () {
188334 // Don't lint empty values
@@ -214,7 +360,10 @@ export default {
214360 return this .emit (updateContext ? null : " " );
215361 case ' json' :
216362 if (value) {
217- return this .emit (JSON .parse (value));
363+ if (typeof value === ' string' ) {
364+ value = JSON .parse (value);
365+ }
366+ return this .emit (value);
218367 }
219368 else {
220369 return this .emit (null );
@@ -256,7 +405,11 @@ export default {
256405 }
257406 break ;
258407 case ' json' :
259- this .insert (JSON .stringify (this .value , null , this .editorOptions .indentUnit ));
408+ let value = this .value ;
409+ if (typeof this .value !== ' string' ) {
410+ value = JSON .stringify (this .value , null , this .editorOptions .indentUnit );
411+ }
412+ this .insert (value);
260413 break ;
261414 default :
262415 this .insert (this .value );
@@ -293,6 +446,11 @@ export default {
293446 height : 100% ;
294447 overflow : hidden ;
295448}
449+ .language-chooser {
450+ padding : 2px 4px ;
451+ font-size : 0.9em ;
452+ width : auto !important ;
453+ }
296454 </style >
297455<style >
298456.textEditor.math .cm-operator {
0 commit comments