diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..bd08d66e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": false, + "tabWidth": 2 + +} diff --git a/app/config.php b/app/config.php index ff439c82..7f1ad4e6 100644 --- a/app/config.php +++ b/app/config.php @@ -182,8 +182,8 @@ ] ); - $renderer->addJs('jquery', '//code.jquery.com/jquery-1.12.0.min.js'); - $renderer->addJs('highlight.js', '//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js'); + //$renderer->addJs('jquery', '//code.jquery.com/jquery-1.12.0.min.js'); + //$renderer->addJs('highlight.js', '//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js'); $manifest = $c->get(ViteManifest::class); $renderer->addJs('main-js', $manifest->assetUrl('main.js')); diff --git a/assets/cloud.js b/assets/cloud.js index 836c278e..d8d97ab4 100644 --- a/assets/cloud.js +++ b/assets/cloud.js @@ -19,6 +19,7 @@ import VueShepherdPlugin from './shepherd-plugin'; import results from "./components/results/results.js"; import StudentDropdown from "./components/StudentDropdown.vue"; import ListWorkshops from "./components/ListWorkshops.vue"; +import Home from "./components/Home.vue"; const components = { AceEditor, @@ -32,7 +33,8 @@ const components = { ExerciseVerify, ExerciseEditor, StudentDropdown, - ListWorkshops + ListWorkshops, + Home } const app = createApp({ diff --git a/assets/components/AceEditor.vue b/assets/components/AceEditor.vue index 51dc888e..256febc0 100644 --- a/assets/components/AceEditor.vue +++ b/assets/components/AceEditor.vue @@ -3,6 +3,8 @@ import ace from 'ace-builds'; import 'ace-builds/src-noconflict/theme-monokai'; import 'ace-builds/src-noconflict/mode-php'; +import 'ace-builds/src-noconflict/ext-language_tools'; +import 'ace-builds/src-noconflict/snippets/php'; import {markRaw} from "vue"; export default { @@ -11,7 +13,10 @@ export default { default: false, type: Boolean }, - file: Object, + value: { + type: String, + required: true, + }, minLines: Number, maxLines: Number, }, @@ -25,12 +30,24 @@ export default { options.maxLines = this.maxLines; } + ace.require("ace/ext/language_tools"); this._editor = markRaw(ace.edit(this.$el, options)); this._editor.setOption('useWorker', false); + this._editor.setShowPrintMargin(false); this._editor.setTheme("ace/theme/monokai"); + this._editor.container.style.lineHeight = 2 this._editor.session.setMode("ace/mode/php"); - this._editor.setValue(this.file.content, 1); + + this._editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + enableLiveAutocompletion: false + }); + + this._editor.setValue(this.value, 1); + this._contentBackup = this.value; + this._isSettingContent = false; if (this.readonly) { this._editor.setReadOnly(true); @@ -38,18 +55,30 @@ export default { this._editor.session.on('change', this.change); } }, - watch: { - 'file.content'(newValue, oldValue) { - if (newValue !== oldValue) { - this._editor.setValue(newValue, 1); - } - } - }, methods: { change() { - this.file.content = this._editor.session.getValue(); - this.$emit('changeContent', this.file); + // ref: https://github.com/CarterLi/vue3-ace-editor/issues/11 + if (this._isSettingContent) { + return; + } + const content = this._editor.session.getValue(); + this._contentBackup = content; + this.$emit('update:value', content); + }, + }, + emits: ['update:value'], + watch: { + value(val) { + if (this._contentBackup !== val) { + try { + this._isSettingContent = true; + this._editor.setValue(val, 1); + } finally { + this._isSettingContent = false; + } + this._contentBackup = val; } + }, }, beforeUnmount() { this._editor.destroy(); @@ -58,5 +87,24 @@ export default { \ No newline at end of file +
+ + + \ No newline at end of file diff --git a/assets/components/ExerciseEditor.vue b/assets/components/ExerciseEditor.vue index e22ecdea..40119994 100644 --- a/assets/components/ExerciseEditor.vue +++ b/assets/components/ExerciseEditor.vue @@ -13,7 +13,8 @@ import { MapIcon, HomeIcon, ChevronRightIcon, - ExclamationCircleIcon + ExclamationCircleIcon, + AcademicCapIcon } from '@heroicons/vue/24/solid'; import {TrophyIcon} from '@heroicons/vue/24/outline' import PackageSearch from './PackageSearch.vue'; @@ -48,6 +49,7 @@ export default { HomeIcon, ChevronRightIcon, ExclamationCircleIcon, + AcademicCapIcon, OutputMismatch, Confirm, }, @@ -63,14 +65,7 @@ export default { links: Object }, mounted() { - const files = this.getSavedFiles(); - for (const fileName in files) { - const fileContent = files[fileName]; - const folderParts = fileName.split("/"); - this.createFileInFolderStructure(this.studentFiles, folderParts, fileContent); - this.studentFiles = this.toTree(this.studentFiles); - } }, data() { //sort the initial files so entry point is at the top @@ -82,7 +77,18 @@ export default { const initialFileCopy = this.initialFiles.map(file => { return {...file} }); - const studentFiles = this.toTree(initialFileCopy); + let studentFiles = this.toTree(initialFileCopy); + + const files = this.getSavedFiles(); + for (const fileName in files) { + const fileContent = files[fileName]; + const folderParts = fileName.split("/"); + + this.createFileInFolderStructure(studentFiles, folderParts, fileContent); + } + + //make sure new files added from saved files have two way relationship + studentFiles = this.toTree(studentFiles); return { firstRunLoaded: false, @@ -122,7 +128,7 @@ export default { methods: { getSavedFiles() { const items = { ...localStorage }; - const key = this.currentExercise.workshop.code + '.' + this.currentExercise.exercise.slug; + const key = this.workshop.code + '.' + this.exercise.slug; const files = {}; for (const localStorageKey in items) { @@ -198,12 +204,12 @@ export default { return subdirectory; }, - saveSolution(file) { + saveSolution(fileContent, file) { const filePath = toFilePath(file); localStorage.setItem( this.currentExercise.workshop.code + '.' + this.currentExercise.exercise.slug + '.' + filePath, - file.content + fileContent ); }, resetState() { @@ -244,6 +250,16 @@ export default { return; } + if (!selectedFile.content) { + selectedFile.content = ''; + + if (selectedFile.name.endsWith('.php')) { + selectedFile.content = ' file === selectedFile); if (!found) { @@ -316,6 +332,10 @@ export default { this.loadingResults = false; }, closeTab(tab) { + if (this.openFiles.length === 1) { + return; + } + let index = this.openFiles.findIndex(file => file.name === tab); this.openFiles.splice(index, 1); @@ -449,8 +469,8 @@ export default {
-
- +
-
+
-
-
-

Results

+
+
+

Results

-
+
diff --git a/assets/components/FileTree.vue b/assets/components/FileTree.vue index 3fdee948..82e5098c 100644 --- a/assets/components/FileTree.vue +++ b/assets/components/FileTree.vue @@ -71,8 +71,8 @@ export default {