@tekkare/romulus 0.1.230 → 0.1.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/DocumentViewer.vue +521 -0
- package/dist/runtime/components/ProjectAddButton.vue +226 -0
- package/dist/runtime/components/ProjectBanner.vue +134 -0
- package/dist/runtime/components/ProjectComments.vue +110 -0
- package/dist/runtime/components/ProjectCreateDialog.vue +117 -0
- package/dist/runtime/components/ProjectDeleteDialog.vue +60 -0
- package/dist/runtime/components/ProjectList.vue +115 -0
- package/dist/runtime/components/ProjectOverview.vue +294 -0
- package/dist/runtime/components/ProjectPanel.vue +193 -0
- package/dist/runtime/components/ProjectRenameDialog.vue +67 -0
- package/dist/runtime/components/ProjectResources.vue +258 -0
- package/dist/runtime/utils/projects.d.ts +200 -0
- package/dist/runtime/utils/projects.js +123 -0
- package/package.json +2 -2
- package/dist/runtime/components/PdfViewer.vue +0 -294
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
RmPdfViewer - un PDF affiche dans la page, avec une recherche dans son texte.
|
|
3
|
-
|
|
4
|
-
Un PDF pose dans une `<iframe>` passe par le lecteur du navigateur : la page
|
|
5
|
-
n'a aucune prise dessus, et quand le fichier vient d'un autre domaine elle
|
|
6
|
-
ne peut meme pas lire son texte. Pour un compte rendu de deux cents pages,
|
|
7
|
-
la seule recherche possible etait Ctrl+F, que personne ne pense a faire dans
|
|
8
|
-
un tiroir, et qui ne savait rien des termes deja tapes dans la page.
|
|
9
|
-
|
|
10
|
-
Ici le rendu passe par pdf.js : les pages sont dessinees a la demande, une
|
|
11
|
-
couche de texte se pose dessus, et la barre en tete cherche dans ce texte
|
|
12
|
-
(surlignage de toutes les occurrences, compteur, precedent / suivant).
|
|
13
|
-
|
|
14
|
-
`query` sert de terme de depart, pour ouvrir le document directement sur
|
|
15
|
-
les passages qui l'ont fait remonter dans une liste de resultats. Une liste
|
|
16
|
-
cherche chaque terme separement ; une chaine, et tout ce que l'utilisateur
|
|
17
|
-
tape ensuite, cherche une expression. La recherche ignore la casse et les
|
|
18
|
-
accents, comme `RmTextHighlight`.
|
|
19
|
-
|
|
20
|
-
Le fichier est lu par `fetch` : il doit venir de la meme origine ou d'un
|
|
21
|
-
serveur qui autorise CORS. Une app qui affiche des documents tiers les fait
|
|
22
|
-
passer par son propre proxy. Si le fichier ne se lit pas (pas un PDF, CORS,
|
|
23
|
-
404), le composant emet `error` : a l'appelant de retomber sur une
|
|
24
|
-
`<iframe>`, qui sait au moins afficher une page HTML.
|
|
25
|
-
|
|
26
|
-
pdf.js (~1 Mo avec son worker) n'est charge qu'au montage du composant.
|
|
27
|
-
-->
|
|
28
|
-
<script setup lang="ts">
|
|
29
|
-
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
30
|
-
|
|
31
|
-
const props = withDefaults(defineProps<{
|
|
32
|
-
/** Adresse du PDF, lue par `fetch`. */
|
|
33
|
-
src: string
|
|
34
|
-
/** Terme(s) cherche(s) a l'ouverture. Une liste cherche chaque terme. */
|
|
35
|
-
query?: string | readonly string[] | null
|
|
36
|
-
lang?: 'fr' | 'en'
|
|
37
|
-
}>(), {
|
|
38
|
-
query: null,
|
|
39
|
-
lang: 'fr',
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const emit = defineEmits<{ error: [error: unknown] }>()
|
|
43
|
-
|
|
44
|
-
const WORDS = {
|
|
45
|
-
fr: {
|
|
46
|
-
search: 'Rechercher dans le document',
|
|
47
|
-
previous: 'Occurrence précédente',
|
|
48
|
-
next: 'Occurrence suivante',
|
|
49
|
-
none: 'Aucun résultat',
|
|
50
|
-
loading: 'Chargement du document…',
|
|
51
|
-
failed: 'Le document ne s’affiche pas.',
|
|
52
|
-
count: (current: number, total: number) => `${current} sur ${total}`,
|
|
53
|
-
},
|
|
54
|
-
en: {
|
|
55
|
-
search: 'Search in document',
|
|
56
|
-
previous: 'Previous match',
|
|
57
|
-
next: 'Next match',
|
|
58
|
-
none: 'No match',
|
|
59
|
-
loading: 'Loading document…',
|
|
60
|
-
failed: 'The document cannot be displayed.',
|
|
61
|
-
count: (current: number, total: number) => `${current} of ${total}`,
|
|
62
|
-
},
|
|
63
|
-
} as const
|
|
64
|
-
|
|
65
|
-
const words = computed(() => WORDS[props.lang])
|
|
66
|
-
|
|
67
|
-
const scroller = ref<HTMLDivElement | null>(null)
|
|
68
|
-
const input = ref<HTMLInputElement | null>(null)
|
|
69
|
-
|
|
70
|
-
const status = ref<'loading' | 'ready' | 'failed'>('loading')
|
|
71
|
-
/** Ce qui est dans le champ. */
|
|
72
|
-
const text = ref('')
|
|
73
|
-
/**
|
|
74
|
-
* Les termes de `query` tant que le champ n'a pas ete retouche : une liste
|
|
75
|
-
* affichee « a b » ne doit pas se chercher comme l'expression « a b ».
|
|
76
|
-
*/
|
|
77
|
-
const terms = shallowRef<string[] | null>(null)
|
|
78
|
-
const matches = ref({ current: 0, total: 0 })
|
|
79
|
-
const pending = ref(false)
|
|
80
|
-
|
|
81
|
-
// Objets pdf.js : hors de la reactivite, Vue n'a rien a y suivre.
|
|
82
|
-
let eventBus: any = null
|
|
83
|
-
let viewer: any = null
|
|
84
|
-
let loadingTask: any = null
|
|
85
|
-
let resizeObserver: ResizeObserver | null = null
|
|
86
|
-
|
|
87
|
-
function seed() {
|
|
88
|
-
const q = props.query
|
|
89
|
-
if (Array.isArray(q)) {
|
|
90
|
-
const list = q.map(x => String(x).trim()).filter(Boolean)
|
|
91
|
-
terms.value = list.length > 1 ? list : null
|
|
92
|
-
text.value = list.join(' ')
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
terms.value = null
|
|
96
|
-
text.value = String(q ?? '').trim()
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Ce que pdf.js cherche : la liste de depart, ou l'expression tapee. */
|
|
101
|
-
const searched = computed<string | string[]>(() => terms.value ?? text.value.trim())
|
|
102
|
-
const hasQuery = computed(() => (Array.isArray(searched.value) ? searched.value.length > 0 : searched.value !== ''))
|
|
103
|
-
|
|
104
|
-
function find(type: '' | 'again' = '', findPrevious = false) {
|
|
105
|
-
if (!eventBus) return
|
|
106
|
-
eventBus.dispatch('find', {
|
|
107
|
-
source: null,
|
|
108
|
-
type,
|
|
109
|
-
query: searched.value,
|
|
110
|
-
caseSensitive: false,
|
|
111
|
-
entireWord: false,
|
|
112
|
-
highlightAll: true,
|
|
113
|
-
findPrevious,
|
|
114
|
-
matchDiacritics: false,
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function onInput(event: Event) {
|
|
119
|
-
text.value = (event.target as HTMLInputElement).value
|
|
120
|
-
terms.value = null
|
|
121
|
-
find()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function step(previous: boolean) {
|
|
125
|
-
if (hasQuery.value) find('again', previous)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function onKeydown(event: KeyboardEvent) {
|
|
129
|
-
if (event.key !== 'Enter') return
|
|
130
|
-
event.preventDefault()
|
|
131
|
-
step(event.shiftKey)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Ctrl/Cmd+F dans le lecteur va au champ plutot qu'a la recherche de la page. */
|
|
135
|
-
function onRootKeydown(event: KeyboardEvent) {
|
|
136
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'f') {
|
|
137
|
-
event.preventDefault()
|
|
138
|
-
input.value?.focus()
|
|
139
|
-
input.value?.select()
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const countLabel = computed(() => {
|
|
144
|
-
if (status.value !== 'ready' || !hasQuery.value || pending.value) return ''
|
|
145
|
-
return matches.value.total
|
|
146
|
-
? words.value.count(matches.value.current, matches.value.total)
|
|
147
|
-
: words.value.none
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
async function load() {
|
|
151
|
-
destroy()
|
|
152
|
-
status.value = 'loading'
|
|
153
|
-
matches.value = { current: 0, total: 0 }
|
|
154
|
-
seed()
|
|
155
|
-
await nextTick()
|
|
156
|
-
const container = scroller.value
|
|
157
|
-
if (!container || !props.src) return
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const pdfjs: any = await import('pdfjs-dist')
|
|
161
|
-
// Le visualiseur lit la bibliotheque sur `globalThis` : elle doit etre la
|
|
162
|
-
// avant son import.
|
|
163
|
-
;(globalThis as any).pdfjsLib ??= pdfjs
|
|
164
|
-
const [{ default: workerSrc }, viewerLib] = await Promise.all([
|
|
165
|
-
// @ts-ignore import d URL, resolu par Vite cote app
|
|
166
|
-
import('pdfjs-dist/build/pdf.worker.min.mjs?url'),
|
|
167
|
-
import('pdfjs-dist/web/pdf_viewer.mjs'),
|
|
168
|
-
// @ts-ignore feuille de style, resolue par Vite cote app
|
|
169
|
-
import('pdfjs-dist/web/pdf_viewer.css'),
|
|
170
|
-
])
|
|
171
|
-
pdfjs.GlobalWorkerOptions.workerSrc ||= workerSrc
|
|
172
|
-
|
|
173
|
-
const { EventBus, PDFLinkService, PDFFindController, PDFViewer } = viewerLib as any
|
|
174
|
-
eventBus = new EventBus()
|
|
175
|
-
const linkService = new PDFLinkService({ eventBus })
|
|
176
|
-
const findController = new PDFFindController({ eventBus, linkService })
|
|
177
|
-
viewer = new PDFViewer({
|
|
178
|
-
container,
|
|
179
|
-
eventBus,
|
|
180
|
-
linkService,
|
|
181
|
-
findController,
|
|
182
|
-
removePageBorders: true,
|
|
183
|
-
})
|
|
184
|
-
linkService.setViewer(viewer)
|
|
185
|
-
|
|
186
|
-
eventBus.on('pagesinit', () => {
|
|
187
|
-
viewer.currentScaleValue = 'page-width'
|
|
188
|
-
if (hasQuery.value) find()
|
|
189
|
-
})
|
|
190
|
-
eventBus.on('updatefindcontrolstate', (e: any) => {
|
|
191
|
-
pending.value = e.state === 3
|
|
192
|
-
matches.value = e.matchesCount
|
|
193
|
-
})
|
|
194
|
-
eventBus.on('updatefindmatchescount', (e: any) => {
|
|
195
|
-
matches.value = e.matchesCount
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
loadingTask = pdfjs.getDocument({ url: props.src })
|
|
199
|
-
const pdf = await loadingTask.promise
|
|
200
|
-
viewer.setDocument(pdf)
|
|
201
|
-
linkService.setDocument(pdf)
|
|
202
|
-
status.value = 'ready'
|
|
203
|
-
|
|
204
|
-
resizeObserver = new ResizeObserver(() => {
|
|
205
|
-
if (viewer?.pagesCount) viewer.currentScaleValue = 'page-width'
|
|
206
|
-
})
|
|
207
|
-
resizeObserver.observe(container)
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
// Une annulation (fermeture pendant le chargement) n'est pas une erreur.
|
|
211
|
-
if ((error as any)?.name === 'AbortException') return
|
|
212
|
-
status.value = 'failed'
|
|
213
|
-
emit('error', error)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function destroy() {
|
|
218
|
-
resizeObserver?.disconnect()
|
|
219
|
-
resizeObserver = null
|
|
220
|
-
loadingTask?.destroy()
|
|
221
|
-
loadingTask = null
|
|
222
|
-
viewer?.cleanup?.()
|
|
223
|
-
viewer = null
|
|
224
|
-
eventBus = null
|
|
225
|
-
if (scroller.value) {
|
|
226
|
-
const inner = scroller.value.firstElementChild
|
|
227
|
-
if (inner) inner.innerHTML = ''
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
watch(() => props.src, load)
|
|
232
|
-
watch(() => props.query, () => {
|
|
233
|
-
seed()
|
|
234
|
-
find()
|
|
235
|
-
})
|
|
236
|
-
onMounted(load)
|
|
237
|
-
onBeforeUnmount(destroy)
|
|
238
|
-
</script>
|
|
239
|
-
|
|
240
|
-
<template>
|
|
241
|
-
<div class="rm-pdf-viewer" @keydown="onRootKeydown">
|
|
242
|
-
<div class="rm-pdf-viewer__bar" role="search">
|
|
243
|
-
<label class="rm-pdf-viewer__field">
|
|
244
|
-
<RmIcon name="MagnifyingGlass" :size="14" color="var(--text-muted)" />
|
|
245
|
-
<input
|
|
246
|
-
ref="input"
|
|
247
|
-
class="rm-pdf-viewer__input"
|
|
248
|
-
type="search"
|
|
249
|
-
:value="text"
|
|
250
|
-
:placeholder="words.search"
|
|
251
|
-
:aria-label="words.search"
|
|
252
|
-
:disabled="status !== 'ready'"
|
|
253
|
-
@input="onInput"
|
|
254
|
-
@keydown="onKeydown"
|
|
255
|
-
>
|
|
256
|
-
</label>
|
|
257
|
-
<span class="rm-pdf-viewer__count" aria-live="polite">{{ countLabel }}</span>
|
|
258
|
-
<RmButton
|
|
259
|
-
ghost
|
|
260
|
-
icon-only
|
|
261
|
-
size="sm"
|
|
262
|
-
prefix-icon="CaretUp"
|
|
263
|
-
:aria-label="words.previous"
|
|
264
|
-
:title="words.previous"
|
|
265
|
-
:disabled="!matches.total"
|
|
266
|
-
@click="step(true)"
|
|
267
|
-
/>
|
|
268
|
-
<RmButton
|
|
269
|
-
ghost
|
|
270
|
-
icon-only
|
|
271
|
-
size="sm"
|
|
272
|
-
prefix-icon="CaretDown"
|
|
273
|
-
:aria-label="words.next"
|
|
274
|
-
:title="words.next"
|
|
275
|
-
:disabled="!matches.total"
|
|
276
|
-
@click="step(false)"
|
|
277
|
-
/>
|
|
278
|
-
</div>
|
|
279
|
-
|
|
280
|
-
<div class="rm-pdf-viewer__frame">
|
|
281
|
-
<!-- pdf.js exige un conteneur positionne en absolu qui defile, et un
|
|
282
|
-
enfant `.pdfViewer` ou il pose les pages. -->
|
|
283
|
-
<div ref="scroller" class="rm-pdf-viewer__scroller">
|
|
284
|
-
<div class="pdfViewer" />
|
|
285
|
-
</div>
|
|
286
|
-
<p v-if="status === 'loading'" class="rm-pdf-viewer__state">{{ words.loading }}</p>
|
|
287
|
-
<p v-else-if="status === 'failed'" class="rm-pdf-viewer__state">{{ words.failed }}</p>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
</template>
|
|
291
|
-
|
|
292
|
-
<style scoped>
|
|
293
|
-
.rm-pdf-viewer{display:flex;flex-direction:column;gap:var(--space-8);height:100%;min-height:60vh}.rm-pdf-viewer__bar{align-items:center;display:flex;gap:var(--space-4)}.rm-pdf-viewer__field{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);display:flex;flex:1;gap:var(--space-8);height:32px;max-width:420px;min-width:0;padding:0 var(--space-12)}.rm-pdf-viewer__field:focus-within{border-color:var(--color-secondary);box-shadow:var(--shadow-focus)}.rm-pdf-viewer__input{background:none;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-13);min-width:0;outline:none}.rm-pdf-viewer__input::-moz-placeholder{color:var(--text-muted)}.rm-pdf-viewer__input::placeholder{color:var(--text-muted)}.rm-pdf-viewer__count{color:var(--text-secondary);font-size:var(--font-12);font-variant-numeric:tabular-nums;margin-left:var(--space-8);min-width:80px}.rm-pdf-viewer__frame{background:var(--bg-surface-2);border-radius:var(--radius-8,8px);flex:1;min-height:0;position:relative}.rm-pdf-viewer__scroller{inset:0;overflow:auto;position:absolute;--highlight-bg-color:color-mix(in srgb,var(--color-primary) 30%,transparent);--highlight-selected-bg-color:color-mix(in srgb,var(--color-primary) 65%,transparent)}.rm-pdf-viewer__scroller :deep(.pdfViewer .page){box-shadow:0 2px 8px rgba(var(--shadow-rgb),12%);margin:var(--space-12) auto}.rm-pdf-viewer__state{color:var(--text-secondary);display:grid;font-size:var(--font-13);inset:0;margin:0;place-items:center;position:absolute}
|
|
294
|
-
</style>
|