|
2 | 2 | Panel which displays a submission files with its line of code.
|
3 | 3 | -->
|
4 | 4 | <template>
|
5 |
| - <Interactable |
6 |
| - :id=" |
7 |
| - panelId |
8 |
| - ?.toString() |
9 |
| - .concat(filePath || '') |
10 |
| - .concat(fileIndex?.toString() || '-1') |
11 |
| - " |
12 |
| - class="mx-2 !shadow" |
13 |
| - > |
14 |
| - <div @click="$emit('toggleCollapse')" class="text-center font-bold"> |
15 |
| - {{ title }} |
| 5 | + <Interactable class="mx-2 !shadow"> |
| 6 | + <div @click="collapsed = !collapsed" class="text-center font-bold"> |
| 7 | + {{ getFileDisplayName(file) }} |
16 | 8 | </div>
|
| 9 | + |
17 | 10 | <div class="mx-1 overflow-x-auto">
|
18 |
| - <div :class="{ hidden: !collapse }" class="w-fit min-w-full"> |
19 |
| - <div v-if="!isEmpty(lines)" class="flex w-full flex-col items-start p-0"> |
20 |
| - <div |
21 |
| - class="flex w-full flex-row" |
22 |
| - v-for="(line, index) in lines" |
23 |
| - :id=" |
24 |
| - String(panelId) |
25 |
| - .concat(filePath || '') |
26 |
| - .concat((index + 1).toString()) |
27 |
| - " |
| 11 | + <div v-if="!collapsed" class="w-fit min-w-full !text-xs"> |
| 12 | + <table |
| 13 | + v-if="file.data.trim() !== ''" |
| 14 | + class="w-full" |
| 15 | + :aria-describedby="`Content of file ${file.fileName}`" |
| 16 | + > |
| 17 | + <!-- One row in table per code line --> |
| 18 | + <tr |
| 19 | + v-for="(line, index) in codeLines" |
28 | 20 | :key="index"
|
| 21 | + class="w-full cursor-default" |
| 22 | + :class="{ 'cursor-pointer': line.match !== null }" |
| 23 | + @click="lineSelected(index)" |
29 | 24 | >
|
30 |
| - <LineOfCode |
31 |
| - class="flex-grow" |
32 |
| - :color="coloringArray[index]" |
33 |
| - :line-number="index + 1" |
34 |
| - :text="line" |
35 |
| - :visible="collapse" |
36 |
| - @click=" |
37 |
| - $emit( |
38 |
| - 'lineSelected', |
39 |
| - $event, |
40 |
| - linksArray[index].panel, |
41 |
| - linksArray[index].file, |
42 |
| - linksArray[index].line |
43 |
| - ) |
44 |
| - " |
45 |
| - /> |
46 |
| - </div> |
47 |
| - </div> |
| 25 | + <!-- Line number --> |
| 26 | + <td class="float-right pr-3">{{ index + 1 }}</td> |
| 27 | + <!-- Code line --> |
| 28 | + <td |
| 29 | + class="w-full" |
| 30 | + :style="{ |
| 31 | + background: line.match !== null ? line.match.color : 'hsla(0, 0%, 0%, 0)' |
| 32 | + }" |
| 33 | + > |
| 34 | + <pre v-html="line.line" class="code-font !bg-transparent" ref="lineRefs"></pre> |
| 35 | + </td> |
| 36 | + </tr> |
| 37 | + </table> |
| 38 | + |
48 | 39 | <div v-else class="flex flex-col items-start overflow-x-auto">
|
49 |
| - <p>Empty File</p> |
| 40 | + <i>Empty File</i> |
50 | 41 | </div>
|
51 | 42 | </div>
|
52 | 43 | </div>
|
|
55 | 46 |
|
56 | 47 | <script setup lang="ts">
|
57 | 48 | import type { MatchInSingleFile } from '@/model/MatchInSingleFile'
|
58 |
| -import { computed, ref, type Ref } from 'vue' |
59 |
| -import LineOfCode from '@/components/LineOfCode.vue' |
| 49 | +import { ref, nextTick, type PropType, computed, type Ref } from 'vue' |
60 | 50 | import Interactable from './InteractableComponent.vue'
|
| 51 | +import type { Match } from '@/model/Match' |
| 52 | +import type { SubmissionFile } from '@/stores/state' |
| 53 | +import { highlight } from '@/utils/CodeHighlighter' |
| 54 | +import type { HighlightLanguage } from '@/model/Language' |
61 | 55 |
|
62 | 56 | const props = defineProps({
|
63 |
| - /** |
64 |
| - * Path of the displayed file. |
65 |
| - */ |
66 |
| - filePath: { |
67 |
| - type: String |
68 |
| - }, |
69 |
| - /** |
70 |
| - * Title of the displayed file. |
71 |
| - */ |
72 |
| - title: { |
73 |
| - type: String |
74 |
| - }, |
75 |
| - /** |
76 |
| - * Index of file amongst other files in submission. |
77 |
| - */ |
78 |
| - fileIndex: { |
79 |
| - type: Number |
80 |
| - }, |
81 | 57 | /**
|
82 | 58 | * Code lines of the file.
|
83 |
| - * type: Array<string> |
84 | 59 | */
|
85 |
| - lines: { |
86 |
| - type: Array<string>, |
| 60 | + file: { |
| 61 | + type: Object as PropType<SubmissionFile>, |
87 | 62 | required: true
|
88 | 63 | },
|
89 | 64 | /**
|
90 | 65 | * Matches in the file
|
91 |
| - * type: Array<MatchInSingleFile> |
92 | 66 | */
|
93 | 67 | matches: {
|
94 |
| - type: Array<MatchInSingleFile> |
95 |
| - }, |
96 |
| - /** |
97 |
| - * Id of the FilesContainer. Needed for lines link generation. |
98 |
| - */ |
99 |
| - panelId: { |
100 |
| - type: Number |
| 68 | + type: Array<MatchInSingleFile>, |
| 69 | + required: true |
101 | 70 | },
|
102 | 71 | /**
|
103 |
| - * Indicates whether files is collapsed or not. |
| 72 | + * Language of the file. |
104 | 73 | */
|
105 |
| - collapse: { |
106 |
| - type: Boolean |
| 74 | + highlightLanguage: { |
| 75 | + type: String as PropType<HighlightLanguage>, |
| 76 | + required: true |
107 | 77 | }
|
108 | 78 | })
|
109 | 79 |
|
110 |
| -defineEmits(['lineSelected', 'toggleCollapse']) |
| 80 | +const emit = defineEmits(['lineSelected']) |
111 | 81 |
|
112 |
| -/** |
113 |
| - * An object containing the color of each line in code. Keys are line numbers, values are their color. |
114 |
| - * Example: { |
115 |
| - * ... |
116 |
| - * 100 : "#3333" |
117 |
| - * 101 : "#3333" |
118 |
| - * 102 : "#3333" |
119 |
| - * 103 : "#FFFF" |
120 |
| - * ... |
121 |
| - * } |
122 |
| - */ |
123 |
| -const coloringArray: Ref<Record<number, string>> = ref({}) |
| 82 | +const collapsed = ref(true) |
| 83 | +const lineRefs = ref<HTMLElement[]>([]) |
124 | 84 |
|
125 |
| -/** |
126 |
| - * @param lines Array of lines to check. |
127 |
| - * @returns true if all lines are empty, false otherwise. |
128 |
| - */ |
129 |
| -function isEmpty(lines: string[]) { |
130 |
| - return lines.length === 0 || lines.every((line) => !line.trim()) |
| 85 | +const codeLines: Ref<{ line: string; match: null | Match }[]> = computed(() => |
| 86 | + highlight(props.file.data, props.highlightLanguage).map((line, index) => { |
| 87 | + return { |
| 88 | + line, |
| 89 | + match: props.matches?.find((m) => m.start <= index + 1 && index + 1 <= m.end)?.match ?? null |
| 90 | + } |
| 91 | + }) |
| 92 | +) |
| 93 | + |
| 94 | +function lineSelected(lineIndex: number) { |
| 95 | + if (codeLines.value[lineIndex].match !== null) { |
| 96 | + emit('lineSelected', codeLines.value[lineIndex].match) |
| 97 | + } |
131 | 98 | }
|
132 | 99 |
|
133 | 100 | /**
|
134 |
| - * An object containing an object from which an id is to of the line to which this is linked is constructed. |
135 |
| - * Id object contains panel, file name, first line number of linked matched. |
136 |
| - * Example: { |
137 |
| - * panel: 1, |
138 |
| - * file: "Example.java", |
139 |
| - * line: 121 |
140 |
| - * } |
141 |
| - * Constructed ID (generateLineCodeLink from Utils.ts): 1Example.java121 |
142 |
| - * When a line is clicked it uses this link id |
143 |
| - * to scroll into vie the linked line in the linked file of the other submission. |
144 |
| - * Key is line number, value is id of linked line. |
| 101 | + * Scrolls to the line number in the file. |
| 102 | + * @param lineNumber line number in the file |
145 | 103 | */
|
146 |
| -const linksArray: Ref<Record<number, { panel?: number; file?: string; line?: number }>> = computed( |
147 |
| - () => { |
148 |
| - const links: Record<number, { panel?: number; file?: string; line?: number }> = {} |
149 |
| - |
150 |
| - props.matches?.forEach((m) => { |
151 |
| - for (let i = m.start; i <= m.end; i++) { |
152 |
| - //assign match color to line |
153 |
| - coloringArray.value[i - 1] = m.color |
154 |
| - //assign link object to line. |
155 |
| - linksArray.value[i - 1] = { |
156 |
| - panel: m.linked_panel, |
157 |
| - file: m.linked_file, |
158 |
| - line: m.linked_line |
159 |
| - } |
160 |
| - } |
161 |
| - }) |
| 104 | +function scrollTo(lineNumber: number) { |
| 105 | + collapsed.value = false |
| 106 | + nextTick(function () { |
| 107 | + lineRefs.value[lineNumber - 1].scrollIntoView({ block: 'center' }) |
| 108 | + }) |
| 109 | +} |
162 | 110 |
|
163 |
| - return links |
164 |
| - } |
165 |
| -) |
| 111 | +defineExpose({ |
| 112 | + scrollTo |
| 113 | +}) |
166 | 114 |
|
167 |
| -//assign default values for all line which are not contained in matches |
168 |
| -for (let i = 0; i < props.lines.length; i++) { |
169 |
| - if (!coloringArray.value[i]) { |
170 |
| - coloringArray.value[i] = 'hsla(0, 0%, 0%, 0)' |
171 |
| - linksArray.value[i] = {} |
172 |
| - } |
| 115 | +/** |
| 116 | + * converts the submissionId to the name in the path of file. If the length of path exceeds 40, then the file path displays the abbreviation. |
| 117 | + * @param file submission file |
| 118 | + * @return new path of file |
| 119 | + */ |
| 120 | +function getFileDisplayName(file: SubmissionFile): string { |
| 121 | + const filePathLength = file.fileName.length |
| 122 | + return filePathLength > 40 |
| 123 | + ? '...' + file.fileName.substring(filePathLength - 40, filePathLength) |
| 124 | + : file.fileName |
173 | 125 | }
|
174 | 126 | </script>
|
| 127 | + |
| 128 | +<style scoped> |
| 129 | +.code-font { |
| 130 | + font-family: 'JetBrains Mono NL', monospace !important; |
| 131 | +} |
| 132 | +</style> |
0 commit comments