forked from izu/student-web-if-development-kit
fix(tapops): deteksi format file dan format tag
This commit is contained in:
@@ -35,6 +35,30 @@
|
|||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doc-type-pptx {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-type-xlsx {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-type-txt {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-type-archive {
|
||||||
|
color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-type-image {
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-type-file {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
.doc-title {
|
.doc-title {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -88,7 +112,6 @@
|
|||||||
color: hsl(43, 80%, 35%);
|
color: hsl(43, 80%, 35%);
|
||||||
border: 1px solid hsl(43, 80%, 85%);
|
border: 1px solid hsl(43, 80%, 85%);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
text-transform: lowercase;
|
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ===========================================
|
/* ===========================================
|
||||||
Viewer Page — PDF iframe + DOCX rendered preview
|
Viewer Page — PDF iframe + Office Online preview
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
||||||
.viewer-body {
|
.viewer-body {
|
||||||
@@ -123,39 +123,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-pdf-frame {
|
.viewer-pdf-frame,
|
||||||
|
.viewer-office-frame {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
min-height: calc(100vh - 80px);
|
min-height: calc(100vh - 80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-docx-content {
|
|
||||||
background: white;
|
|
||||||
margin: 24px auto;
|
|
||||||
padding: 60px 80px;
|
|
||||||
max-width: 900px;
|
|
||||||
width: calc(100% - 48px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-x: auto;
|
|
||||||
color: var(--text-dark);
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family: 'Roboto', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* docx-preview injects its own structure; override its defaults gently. */
|
|
||||||
.viewer-docx-content .docx-wrapper {
|
|
||||||
background: transparent !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewer-docx-content .docx {
|
|
||||||
box-shadow: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- States --- */
|
/* --- States --- */
|
||||||
|
|
||||||
.viewer-loading,
|
.viewer-loading,
|
||||||
@@ -183,27 +158,6 @@
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Download Progress Bar --- */
|
|
||||||
|
|
||||||
.viewer-progress-bar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 3px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
z-index: 5;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewer-progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
width: 0%;
|
|
||||||
background: linear-gradient(90deg, var(--secondary-gold), #ffd54f);
|
|
||||||
border-radius: 0 2px 2px 0;
|
|
||||||
transition: width 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Responsive --- */
|
/* --- Responsive --- */
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -220,12 +174,6 @@
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-docx-content {
|
|
||||||
padding: 24px;
|
|
||||||
margin: 12px;
|
|
||||||
width: calc(100% - 24px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back,
|
.btn-back,
|
||||||
.btn-download {
|
.btn-download {
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ export class DocumentCardView {
|
|||||||
this.title = item.nama_berkas;
|
this.title = item.nama_berkas;
|
||||||
this.tags = DocumentFormatter.parseTags(item.tag);
|
this.tags = DocumentFormatter.parseTags(item.tag);
|
||||||
this.category = DocumentFormatter.categoryFromTags(this.tags);
|
this.category = DocumentFormatter.categoryFromTags(this.tags);
|
||||||
this.fileType = DocumentFormatter.fileTypeFromName(this.title);
|
this.fileInfo = DocumentFormatter.fileInfoFromItem(item);
|
||||||
|
this.fileType = this.fileInfo.type;
|
||||||
this.assetUrl = DirectusClient.assetUrl(item.file);
|
this.assetUrl = DirectusClient.assetUrl(item.file);
|
||||||
this.viewerUrl = DirectusClient.viewerUrl(item);
|
this.viewerUrl = DirectusClient.viewerUrl(item);
|
||||||
this.downloadName = DocumentFormatter.downloadFilename(this.title, this.fileType);
|
this.downloadName = DocumentFormatter.downloadFilenameFromItem(item);
|
||||||
this.dateIso = item.tanggal_upload || '';
|
this.dateIso = item.tanggal_upload || '';
|
||||||
this.dateKey = this.dateIso ? this.dateIso.substring(0, 10) : '';
|
this.dateKey = this.dateIso ? this.dateIso.substring(0, 10) : '';
|
||||||
}
|
}
|
||||||
@@ -22,8 +23,8 @@ export class DocumentCardView {
|
|||||||
card.dataset.date = this.dateKey;
|
card.dataset.date = this.dateKey;
|
||||||
|
|
||||||
const typeUpper = this.fileType.toUpperCase();
|
const typeUpper = this.fileType.toUpperCase();
|
||||||
const typeClass = this.fileType === 'pdf' ? 'doc-type-pdf' : 'doc-type-docx';
|
const typeClass = this.fileInfo.css;
|
||||||
const iconClass = this.fileType === 'pdf' ? 'fas fa-file-pdf' : 'fas fa-file-word';
|
const iconClass = this.fileInfo.icon;
|
||||||
const formattedDate = DocumentFormatter.formatDate(this.dateIso);
|
const formattedDate = DocumentFormatter.formatDate(this.dateIso);
|
||||||
|
|
||||||
const safeTitle = DocumentFormatter.escapeHtml(this.title);
|
const safeTitle = DocumentFormatter.escapeHtml(this.title);
|
||||||
@@ -32,7 +33,7 @@ export class DocumentCardView {
|
|||||||
|
|
||||||
// Build tag badges HTML.
|
// Build tag badges HTML.
|
||||||
const tagBadgesHtml = this.tags.length
|
const tagBadgesHtml = this.tags.length
|
||||||
? `<div class="doc-tags"><i class="fas fa-tags doc-tags-icon"></i>${this.tags.map(t => `<span class="doc-tag">${DocumentFormatter.escapeHtml(t)}</span>`).join('')}</div>`
|
? `<div class="doc-tags"><i class="fas fa-tags doc-tags-icon"></i>${this.tags.map(t => `<span class="doc-tag">${DocumentFormatter.escapeHtml(DocumentFormatter.formatTag(t))}</span>`).join('')}</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
|
|||||||
@@ -4,17 +4,26 @@ export class DirectusClient {
|
|||||||
static API_BASE = 'https://api.ifuntanhub.dev';
|
static API_BASE = 'https://api.ifuntanhub.dev';
|
||||||
|
|
||||||
static itemsUrl(collection) {
|
static itemsUrl(collection) {
|
||||||
return `${this.API_BASE}/items/${collection}?limit=-1`;
|
const params = new URLSearchParams({
|
||||||
|
limit: '-1',
|
||||||
|
fields: '*,file.id,file.filename_download,file.type,file.filename_disk',
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${this.API_BASE}/items/${collection}?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static assetUrl(uuid) {
|
static fileId(file) {
|
||||||
return `${this.API_BASE}/assets/${uuid}`;
|
return file && typeof file === 'object' ? file.id : file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static assetUrl(file) {
|
||||||
|
return `${this.API_BASE}/assets/${this.fileId(file)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static viewerUrl(item) {
|
static viewerUrl(item) {
|
||||||
const type = DocumentFormatter.fileTypeFromName(item.nama_berkas);
|
const type = DocumentFormatter.fileInfoFromItem(item).type;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
id: item.file,
|
id: this.fileId(item.file),
|
||||||
name: item.nama_berkas,
|
name: item.nama_berkas,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export class FileDownloader {
|
export class FileDownloader {
|
||||||
static async download(url, filename) {
|
static async download(url, filename) {
|
||||||
|
const downloadUrl = this.originalDownloadUrl(url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(downloadUrl);
|
||||||
if (!response.ok) throw new Error('Gagal mengunduh berkas.');
|
if (!response.ok) throw new Error('Gagal mengunduh berkas.');
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
@@ -9,11 +11,21 @@ export class FileDownloader {
|
|||||||
URL.revokeObjectURL(blobUrl);
|
URL.revokeObjectURL(blobUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Unduh via Blob gagal, beralih ke parameter Directus:', err);
|
console.warn('Unduh via Blob gagal, beralih ke parameter Directus:', err);
|
||||||
const fallbackUrl = url.includes('?') ? `${url}&download` : `${url}?download`;
|
this._click(downloadUrl, filename);
|
||||||
this._click(fallbackUrl, filename);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static originalDownloadUrl(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
const hashIndex = url.indexOf('#');
|
||||||
|
const hash = hashIndex >= 0 ? url.slice(hashIndex) : '';
|
||||||
|
const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
||||||
|
|
||||||
|
if (/(^|[?&])download(?:[=&]|$)/.test(base)) return url;
|
||||||
|
return `${base}${base.includes('?') ? '&' : '?'}download${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
static _click(href, filename) {
|
static _click(href, filename) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.style.display = 'none';
|
a.style.display = 'none';
|
||||||
|
|||||||
@@ -7,6 +7,56 @@ export class DocumentFormatter {
|
|||||||
{ key: 'praktik', match: ['praktik', 'praktek', 'kerja praktik', 'kp'] },
|
{ key: 'praktik', match: ['praktik', 'praktek', 'kerja praktik', 'kp'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static TAG_ABBREVIATIONS = new Set(['kp', 'sop', 'ta', 'krs', 'khs', 'sks', 'pkl', 'mbkm']);
|
||||||
|
|
||||||
|
static FILE_TYPE_RULES = [
|
||||||
|
{ extensions: ['pdf'], mimeTypes: ['application/pdf'], icon: 'fas fa-file-pdf', css: 'doc-type-pdf' },
|
||||||
|
{
|
||||||
|
extensions: ['doc', 'docx'],
|
||||||
|
mimeTypes: [
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
],
|
||||||
|
fallbackType: 'docx',
|
||||||
|
icon: 'fas fa-file-word',
|
||||||
|
css: 'doc-type-docx',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['ppt', 'pptx'],
|
||||||
|
mimeTypes: [
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
],
|
||||||
|
fallbackType: 'pptx',
|
||||||
|
icon: 'fas fa-file-powerpoint',
|
||||||
|
css: 'doc-type-pptx',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['xls', 'xlsx'],
|
||||||
|
mimeTypes: [
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
],
|
||||||
|
fallbackType: 'xlsx',
|
||||||
|
icon: 'fas fa-file-excel',
|
||||||
|
css: 'doc-type-xlsx',
|
||||||
|
},
|
||||||
|
{ extensions: ['csv'], mimeTypes: ['text/csv'], icon: 'fas fa-file-csv', css: 'doc-type-xlsx' },
|
||||||
|
{ extensions: ['txt', 'rtf', 'md'], mimeTypes: ['text/plain', 'application/rtf'], icon: 'fas fa-file-alt', css: 'doc-type-txt' },
|
||||||
|
{
|
||||||
|
extensions: ['zip', 'rar', '7z', 'tar', 'gz'],
|
||||||
|
mimeTypes: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed', 'application/gzip'],
|
||||||
|
icon: 'fas fa-file-archive',
|
||||||
|
css: 'doc-type-archive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'],
|
||||||
|
mimePrefix: 'image/',
|
||||||
|
icon: 'fas fa-file-image',
|
||||||
|
css: 'doc-type-image',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
static formatDate(iso) {
|
static formatDate(iso) {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
const date = new Date(iso);
|
const date = new Date(iso);
|
||||||
@@ -26,14 +76,83 @@ export class DocumentFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fileTypeFromName(name) {
|
static fileTypeFromName(name) {
|
||||||
const lower = (name || '').toLowerCase();
|
return this.fileInfoFromName(name).type;
|
||||||
if (lower.endsWith('.docx') || lower.endsWith('.doc')) return 'docx';
|
}
|
||||||
return 'pdf';
|
|
||||||
|
static fileInfoFromName(name) {
|
||||||
|
const extension = this.extensionFromName(name);
|
||||||
|
const rule = this.FILE_TYPE_RULES.find(item => item.extensions.includes(extension));
|
||||||
|
|
||||||
|
if (!rule) {
|
||||||
|
return {
|
||||||
|
type: extension || 'file',
|
||||||
|
icon: 'fas fa-file',
|
||||||
|
css: 'doc-type-file',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: extension,
|
||||||
|
icon: rule.icon,
|
||||||
|
css: rule.css,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static fileInfoFromItem(item) {
|
||||||
|
const file = item?.file && typeof item.file === 'object' ? item.file : {};
|
||||||
|
const nameCandidates = [
|
||||||
|
file.filename_download,
|
||||||
|
file.filename_disk,
|
||||||
|
item?.nama_berkas,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const name of nameCandidates) {
|
||||||
|
const info = this.fileInfoFromName(name);
|
||||||
|
if (info.type !== 'file') return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fileInfoFromMime(file.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fileInfoFromMime(type) {
|
||||||
|
const mime = String(type || '').trim().toLowerCase();
|
||||||
|
if (!mime) return this.fileInfoFromName('');
|
||||||
|
|
||||||
|
const rule = this.FILE_TYPE_RULES.find(item => (
|
||||||
|
item.mimeTypes?.includes(mime) || (item.mimePrefix && mime.startsWith(item.mimePrefix))
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!rule) return this.fileInfoFromName('');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: rule.fallbackType || rule.extensions[0],
|
||||||
|
icon: rule.icon,
|
||||||
|
css: rule.css,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static extensionFromName(name) {
|
||||||
|
const normalized = String(name || '').trim().toLowerCase();
|
||||||
|
const lastSegment = normalized.split(/[\\/]/).pop() || '';
|
||||||
|
const extensionStart = lastSegment.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (extensionStart <= 0 || extensionStart === lastSegment.length - 1) return '';
|
||||||
|
return lastSegment.slice(extensionStart + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static downloadFilename(name, type) {
|
static downloadFilename(name, type) {
|
||||||
const ext = `.${type}`;
|
const ext = `.${type}`;
|
||||||
return name.toLowerCase().endsWith(ext) ? name : `${name}${ext}`;
|
if (!name) return `dokumen${ext}`;
|
||||||
|
return name.toLowerCase().endsWith(ext.toLowerCase()) ? name : `${name}${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static downloadFilenameFromItem(item) {
|
||||||
|
const file = item?.file && typeof item.file === 'object' ? item.file : {};
|
||||||
|
const originalFilename = file.filename_download || file.filename_disk;
|
||||||
|
if (originalFilename) return originalFilename;
|
||||||
|
|
||||||
|
const fileType = this.fileInfoFromItem(item).type;
|
||||||
|
return this.downloadFilename(item?.nama_berkas || 'dokumen', fileType);
|
||||||
}
|
}
|
||||||
|
|
||||||
static escapeHtml(str) {
|
static escapeHtml(str) {
|
||||||
@@ -50,6 +169,20 @@ export class DocumentFormatter {
|
|||||||
return this.escapeHtml(str);
|
return this.escapeHtml(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static formatTag(tag) {
|
||||||
|
const value = String(tag).trim().replace(/\s+/g, ' ');
|
||||||
|
if (!value) return value;
|
||||||
|
|
||||||
|
return value.split(' ').map(word => {
|
||||||
|
const lower = word.toLowerCase();
|
||||||
|
if (this.TAG_ABBREVIATIONS.has(lower) || /^[A-Z0-9]{2,}$/.test(word)) {
|
||||||
|
return lower.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
||||||
|
}).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
static parseTags(raw) {
|
static parseTags(raw) {
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
const str = String(raw).trim();
|
const str = String(raw).trim();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ScriptLoader } from './script-loader.js';
|
import { FileDownloader } from '../services/file-downloader.js';
|
||||||
import { ViewerApi } from './viewer-api.js';
|
import { ViewerApi } from './viewer-api.js';
|
||||||
import { ViewerProgressBar } from './viewer-progress-bar.js';
|
|
||||||
|
|
||||||
export class FileViewer {
|
export class FileViewer {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -29,10 +28,12 @@ export class FileViewer {
|
|||||||
this._setupDownload();
|
this._setupDownload();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.type === 'docx') {
|
if (this._isOfficeType()) {
|
||||||
await this._renderDocx();
|
this._renderOfficeOnline();
|
||||||
} else {
|
} else if (this.type === 'pdf') {
|
||||||
this._renderPdf();
|
this._renderPdf();
|
||||||
|
} else {
|
||||||
|
this._renderUnsupported();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -44,7 +45,8 @@ export class FileViewer {
|
|||||||
const ext = `.${this.type}`;
|
const ext = `.${this.type}`;
|
||||||
const filename = this.name.toLowerCase().endsWith(ext) ? this.name : `${this.name}${ext}`;
|
const filename = this.name.toLowerCase().endsWith(ext) ? this.name : `${this.name}${ext}`;
|
||||||
|
|
||||||
this.downloadEl.removeAttribute('href');
|
this.downloadEl.href = FileDownloader.originalDownloadUrl(this.assetUrl);
|
||||||
|
this.downloadEl.download = filename;
|
||||||
this.downloadEl.style.cursor = 'pointer';
|
this.downloadEl.style.cursor = 'pointer';
|
||||||
this.downloadEl.addEventListener('click', async event => {
|
this.downloadEl.addEventListener('click', async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -52,19 +54,10 @@ export class FileViewer {
|
|||||||
this.downloadEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Mengunduh…';
|
this.downloadEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Mengunduh…';
|
||||||
this.downloadEl.style.pointerEvents = 'none';
|
this.downloadEl.style.pointerEvents = 'none';
|
||||||
try {
|
try {
|
||||||
const blob = await ViewerApi.fetchBlob(this.assetUrl);
|
await FileDownloader.download(this.assetUrl, filename);
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.style.display = 'none';
|
|
||||||
link.href = blobUrl;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Blob download failed, falling back:', err);
|
console.warn('Blob download failed, falling back:', err);
|
||||||
window.open(`${this.assetUrl}?download`, '_blank');
|
window.open(FileDownloader.originalDownloadUrl(this.assetUrl), '_blank');
|
||||||
} finally {
|
} finally {
|
||||||
this.downloadEl.innerHTML = originalHtml;
|
this.downloadEl.innerHTML = originalHtml;
|
||||||
this.downloadEl.style.pointerEvents = '';
|
this.downloadEl.style.pointerEvents = '';
|
||||||
@@ -72,6 +65,20 @@ export class FileViewer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isOfficeType() {
|
||||||
|
return ['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx'].includes(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderOfficeOnline() {
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.className = 'viewer-office-frame';
|
||||||
|
iframe.title = this.name;
|
||||||
|
iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(this.assetUrl)}`;
|
||||||
|
iframe.setAttribute('frameborder', '0');
|
||||||
|
iframe.setAttribute('allowfullscreen', '');
|
||||||
|
this.contentEl.replaceChildren(iframe);
|
||||||
|
}
|
||||||
|
|
||||||
_renderPdf() {
|
_renderPdf() {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.className = 'viewer-pdf-frame';
|
iframe.className = 'viewer-pdf-frame';
|
||||||
@@ -80,33 +87,18 @@ export class FileViewer {
|
|||||||
this.contentEl.replaceChildren(iframe);
|
this.contentEl.replaceChildren(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _renderDocx() {
|
_renderUnsupported() {
|
||||||
const progressBar = new ViewerProgressBar(this.contentEl);
|
const wrapper = document.createElement('div');
|
||||||
const [blob] = await Promise.all([
|
wrapper.className = 'viewer-error';
|
||||||
ViewerApi.fetchBlob(this.assetUrl, ratio => progressBar.update(ratio)),
|
|
||||||
this._loadDocxLibraries(),
|
|
||||||
]);
|
|
||||||
progressBar.finish();
|
|
||||||
|
|
||||||
if (!window.docx || typeof window.docx.renderAsync !== 'function') {
|
const icon = document.createElement('i');
|
||||||
throw new Error('Library docx-preview belum dimuat.');
|
icon.className = 'fas fa-file';
|
||||||
}
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const span = document.createElement('span');
|
||||||
container.className = 'viewer-docx-content';
|
span.textContent = `Pratinjau tidak tersedia untuk format ${this.type.toUpperCase()}. Silakan unduh berkas.`;
|
||||||
this.contentEl.replaceChildren(container);
|
|
||||||
|
|
||||||
await window.docx.renderAsync(blob, container, null, {
|
wrapper.append(icon, span);
|
||||||
inWrapper: true,
|
this.contentEl.replaceChildren(wrapper);
|
||||||
ignoreWidth: false,
|
|
||||||
ignoreHeight: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _loadDocxLibraries() {
|
|
||||||
if (window.docx) return;
|
|
||||||
await ScriptLoader.load('https://unpkg.com/jszip@3.10.1/dist/jszip.min.js');
|
|
||||||
await ScriptLoader.load('https://unpkg.com/docx-preview@0.3.5/dist/docx-preview.js');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderError(message) {
|
_renderError(message) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export class ScriptLoader {
|
|
||||||
static load(src) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = src;
|
|
||||||
script.onload = resolve;
|
|
||||||
script.onerror = () => reject(new Error(`Gagal memuat skrip: ${src}`));
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export class ViewerProgressBar {
|
|
||||||
constructor(contentEl) {
|
|
||||||
this.bar = document.createElement('div');
|
|
||||||
this.bar.className = 'viewer-progress-bar';
|
|
||||||
this.fill = document.createElement('div');
|
|
||||||
this.fill.className = 'viewer-progress-fill';
|
|
||||||
this.bar.appendChild(this.fill);
|
|
||||||
contentEl.prepend(this.bar);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(ratio) {
|
|
||||||
this.fill.style.width = `${Math.min(Math.round(ratio * 100), 100)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
finish() {
|
|
||||||
this.fill.style.width = '100%';
|
|
||||||
setTimeout(() => this.bar.remove(), 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { DirectusClient } from '../js/services/directus-client.js';
|
||||||
|
|
||||||
|
test('requests berkas with expanded Directus file metadata', () => {
|
||||||
|
const url = new URL(DirectusClient.itemsUrl('berkas'));
|
||||||
|
|
||||||
|
assert.equal(url.pathname, '/items/berkas');
|
||||||
|
assert.equal(url.searchParams.get('limit'), '-1');
|
||||||
|
assert.equal(url.searchParams.get('fields'), '*,file.id,file.filename_download,file.type,file.filename_disk');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds asset and viewer URLs from expanded Directus file objects', () => {
|
||||||
|
const item = {
|
||||||
|
nama_berkas: 'Panduan Penulisan Proposal Kerja Praktek',
|
||||||
|
file: {
|
||||||
|
id: '4976330e-e547-45f6-b9be-afce493c8a82',
|
||||||
|
filename_download: 'Panduan Penulisan Proposal Kerja Praktek.docx',
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
DirectusClient.assetUrl(item.file),
|
||||||
|
'https://api.ifuntanhub.dev/assets/4976330e-e547-45f6-b9be-afce493c8a82',
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewerUrl = new URL(DirectusClient.viewerUrl(item), 'https://example.test/groups/Tapops/');
|
||||||
|
assert.equal(viewerUrl.pathname, '/groups/Tapops/viewer.html');
|
||||||
|
assert.equal(viewerUrl.searchParams.get('id'), '4976330e-e547-45f6-b9be-afce493c8a82');
|
||||||
|
assert.equal(viewerUrl.searchParams.get('name'), 'Panduan Penulisan Proposal Kerja Praktek');
|
||||||
|
assert.equal(viewerUrl.searchParams.get('type'), 'docx');
|
||||||
|
});
|
||||||
@@ -15,6 +15,14 @@ test('parses comma-separated and JSON string tags', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('formats tag labels without breaking abbreviations', () => {
|
||||||
|
assert.equal(DocumentFormatter.formatTag('SOP'), 'SOP');
|
||||||
|
assert.equal(DocumentFormatter.formatTag('kp'), 'KP');
|
||||||
|
assert.equal(DocumentFormatter.formatTag('kerja praktek'), 'Kerja Praktek');
|
||||||
|
assert.equal(DocumentFormatter.formatTag('Kerja Praktek'), 'Kerja Praktek');
|
||||||
|
assert.equal(DocumentFormatter.formatTag('umum'), 'Umum');
|
||||||
|
});
|
||||||
|
|
||||||
test('maps parsed tags to fixed repository categories', () => {
|
test('maps parsed tags to fixed repository categories', () => {
|
||||||
assert.equal(DocumentFormatter.categoryFromTags(['TA']), 'skripsi');
|
assert.equal(DocumentFormatter.categoryFromTags(['TA']), 'skripsi');
|
||||||
assert.equal(DocumentFormatter.categoryFromTags(['Kalender']), 'akademik');
|
assert.equal(DocumentFormatter.categoryFromTags(['Kalender']), 'akademik');
|
||||||
@@ -26,6 +34,78 @@ test('maps parsed tags to fixed repository categories', () => {
|
|||||||
test('derives file type and safe download filename from document name', () => {
|
test('derives file type and safe download filename from document name', () => {
|
||||||
assert.equal(DocumentFormatter.fileTypeFromName('Panduan Akademik.pdf'), 'pdf');
|
assert.equal(DocumentFormatter.fileTypeFromName('Panduan Akademik.pdf'), 'pdf');
|
||||||
assert.equal(DocumentFormatter.fileTypeFromName('Form KP.DOCX'), 'docx');
|
assert.equal(DocumentFormatter.fileTypeFromName('Form KP.DOCX'), 'docx');
|
||||||
|
assert.equal(DocumentFormatter.fileTypeFromName('Catatan.txt'), 'txt');
|
||||||
|
assert.equal(DocumentFormatter.fileTypeFromName('Materi Seminar.pptx'), 'pptx');
|
||||||
assert.equal(DocumentFormatter.downloadFilename('Form KP', 'docx'), 'Form KP.docx');
|
assert.equal(DocumentFormatter.downloadFilename('Form KP', 'docx'), 'Form KP.docx');
|
||||||
|
assert.equal(DocumentFormatter.downloadFilename('Template lama.DOC', 'doc'), 'Template lama.DOC');
|
||||||
assert.equal(DocumentFormatter.downloadFilename('Kalender.pdf', 'pdf'), 'Kalender.pdf');
|
assert.equal(DocumentFormatter.downloadFilename('Kalender.pdf', 'pdf'), 'Kalender.pdf');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('maps supported file names to display metadata for cards', () => {
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromName('Panduan Akademik.pdf'), {
|
||||||
|
type: 'pdf',
|
||||||
|
icon: 'fas fa-file-pdf',
|
||||||
|
css: 'doc-type-pdf',
|
||||||
|
});
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromName('Form KP.DOCX'), {
|
||||||
|
type: 'docx',
|
||||||
|
icon: 'fas fa-file-word',
|
||||||
|
css: 'doc-type-docx',
|
||||||
|
});
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromName('Catatan.txt'), {
|
||||||
|
type: 'txt',
|
||||||
|
icon: 'fas fa-file-alt',
|
||||||
|
css: 'doc-type-txt',
|
||||||
|
});
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromName('Materi Seminar.pptx'), {
|
||||||
|
type: 'pptx',
|
||||||
|
icon: 'fas fa-file-powerpoint',
|
||||||
|
css: 'doc-type-pptx',
|
||||||
|
});
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromName('Lampiran.7z'), {
|
||||||
|
type: '7z',
|
||||||
|
icon: 'fas fa-file-archive',
|
||||||
|
css: 'doc-type-archive',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('derives file metadata from expanded Directus file records when display title has no extension', () => {
|
||||||
|
const pdfItem = {
|
||||||
|
nama_berkas: 'SOP Kerja Praktek',
|
||||||
|
file: {
|
||||||
|
filename_download: 'SOP Kerja Praktek.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const docxItem = {
|
||||||
|
nama_berkas: 'Panduan Penulisan Proposal Kerja Praktek',
|
||||||
|
file: {
|
||||||
|
filename_download: 'Panduan Penulisan Proposal Kerja Praktek.docx',
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromItem(pdfItem), {
|
||||||
|
type: 'pdf',
|
||||||
|
icon: 'fas fa-file-pdf',
|
||||||
|
css: 'doc-type-pdf',
|
||||||
|
});
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromItem(docxItem), {
|
||||||
|
type: 'docx',
|
||||||
|
icon: 'fas fa-file-word',
|
||||||
|
css: 'doc-type-docx',
|
||||||
|
});
|
||||||
|
assert.equal(DocumentFormatter.downloadFilenameFromItem(pdfItem), 'SOP Kerja Praktek.pdf');
|
||||||
|
assert.equal(DocumentFormatter.downloadFilenameFromItem(docxItem), 'Panduan Penulisan Proposal Kerja Praktek.docx');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to MIME type when Directus filename metadata has no extension', () => {
|
||||||
|
assert.deepEqual(DocumentFormatter.fileInfoFromItem({
|
||||||
|
nama_berkas: 'Berkas Presentasi',
|
||||||
|
file: { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
|
||||||
|
}), {
|
||||||
|
type: 'pptx',
|
||||||
|
icon: 'fas fa-file-powerpoint',
|
||||||
|
css: 'doc-type-pptx',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { FileDownloader } from '../js/services/file-downloader.js';
|
||||||
|
import { FileViewer } from '../js/viewer/file-viewer.js';
|
||||||
|
|
||||||
|
class FakeElement {
|
||||||
|
constructor(tagName = 'div') {
|
||||||
|
this.tagName = tagName.toUpperCase();
|
||||||
|
this.children = [];
|
||||||
|
this.attributes = new Map();
|
||||||
|
this.dataset = {};
|
||||||
|
this.eventListeners = new Map();
|
||||||
|
this.style = {};
|
||||||
|
this.className = '';
|
||||||
|
this.href = '';
|
||||||
|
this.src = '';
|
||||||
|
this.title = '';
|
||||||
|
this.textContent = '';
|
||||||
|
this.innerHTML = '';
|
||||||
|
this.download = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
append(...nodes) {
|
||||||
|
this.children.push(...nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChild(node) {
|
||||||
|
this.children.push(node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepend(node) {
|
||||||
|
this.children.unshift(node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChild(node) {
|
||||||
|
this.children = this.children.filter(child => child !== node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceChildren(...nodes) {
|
||||||
|
this.children = [...nodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribute(name, value) {
|
||||||
|
this.attributes.set(name, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(name) {
|
||||||
|
return this.attributes.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAttribute(name) {
|
||||||
|
this.attributes.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(type, handler) {
|
||||||
|
this.eventListeners.set(type, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
click() {
|
||||||
|
this.clicked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installViewerDom(search) {
|
||||||
|
const previous = {
|
||||||
|
document: globalThis.document,
|
||||||
|
window: globalThis.window,
|
||||||
|
URL: globalThis.URL,
|
||||||
|
fetch: globalThis.fetch,
|
||||||
|
setTimeout: globalThis.setTimeout,
|
||||||
|
consoleError: console.error,
|
||||||
|
consoleWarn: console.warn,
|
||||||
|
};
|
||||||
|
const elements = {
|
||||||
|
title: new FakeElement('h1'),
|
||||||
|
type: new FakeElement('span'),
|
||||||
|
download: new FakeElement('a'),
|
||||||
|
content: new FakeElement('main'),
|
||||||
|
};
|
||||||
|
const body = new FakeElement('body');
|
||||||
|
const created = [];
|
||||||
|
|
||||||
|
globalThis.document = {
|
||||||
|
title: '',
|
||||||
|
body,
|
||||||
|
getElementById(id) {
|
||||||
|
return {
|
||||||
|
'viewer-title': elements.title,
|
||||||
|
'viewer-type': elements.type,
|
||||||
|
'viewer-download': elements.download,
|
||||||
|
'viewer-content': elements.content,
|
||||||
|
}[id];
|
||||||
|
},
|
||||||
|
createElement(tagName) {
|
||||||
|
const node = new FakeElement(tagName);
|
||||||
|
created.push(node);
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
globalThis.window = {
|
||||||
|
location: { search },
|
||||||
|
open(url) {
|
||||||
|
this.openedUrl = url;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
globalThis.URL = {
|
||||||
|
createObjectURL() {
|
||||||
|
return 'blob:tapops-download';
|
||||||
|
},
|
||||||
|
revokeObjectURL(url) {
|
||||||
|
this.revokedUrl = url;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
globalThis.setTimeout = callback => {
|
||||||
|
callback();
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
console.error = () => {};
|
||||||
|
console.warn = () => {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
created,
|
||||||
|
restore() {
|
||||||
|
globalThis.document = previous.document;
|
||||||
|
globalThis.window = previous.window;
|
||||||
|
globalThis.URL = previous.URL;
|
||||||
|
globalThis.fetch = previous.fetch;
|
||||||
|
globalThis.setTimeout = previous.setTimeout;
|
||||||
|
console.error = previous.consoleError;
|
||||||
|
console.warn = previous.consoleWarn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('repository download fetches the Directus original-file URL', async (t) => {
|
||||||
|
const { created, restore } = installViewerDom('');
|
||||||
|
t.after(restore);
|
||||||
|
|
||||||
|
let fetchedUrl = '';
|
||||||
|
globalThis.fetch = async (url) => {
|
||||||
|
fetchedUrl = url;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
blob: async () => new Blob(['docx']),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await FileDownloader.download('https://api.ifuntanhub.dev/assets/docx-id', 'Form KP.docx');
|
||||||
|
|
||||||
|
const clickedAnchor = created.find(node => node.tagName === 'A' && node.clicked);
|
||||||
|
assert.equal(fetchedUrl, 'https://api.ifuntanhub.dev/assets/docx-id?download');
|
||||||
|
assert.equal(clickedAnchor.download, 'Form KP.docx');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DOCX preview renders Microsoft Office Online Viewer iframe', async (t) => {
|
||||||
|
const { elements, restore } = installViewerDom('?id=docx-id&name=Form%20KP.docx&type=docx');
|
||||||
|
t.after(restore);
|
||||||
|
|
||||||
|
await new FileViewer().init();
|
||||||
|
|
||||||
|
const iframe = elements.content.children[0];
|
||||||
|
const assetUrl = 'https://api.ifuntanhub.dev/assets/docx-id';
|
||||||
|
assert.equal(iframe.tagName, 'IFRAME');
|
||||||
|
assert.equal(iframe.className, 'viewer-office-frame');
|
||||||
|
assert.equal(iframe.src, `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(assetUrl)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('viewer download uses the Directus original-file URL', async (t) => {
|
||||||
|
const { elements, created, restore } = installViewerDom('?id=pdf-id&name=Panduan.pdf&type=pdf');
|
||||||
|
t.after(restore);
|
||||||
|
|
||||||
|
let fetchedUrl = '';
|
||||||
|
globalThis.fetch = async (url) => {
|
||||||
|
fetchedUrl = url;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
headers: { get: () => null },
|
||||||
|
blob: async () => new Blob(['pdf']),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await new FileViewer().init();
|
||||||
|
await elements.download.eventListeners.get('click')({ preventDefault() {} });
|
||||||
|
|
||||||
|
const clickedAnchor = created.find(node => node.tagName === 'A' && node.clicked);
|
||||||
|
assert.equal(fetchedUrl, 'https://api.ifuntanhub.dev/assets/pdf-id?download');
|
||||||
|
assert.equal(clickedAnchor.download, 'Panduan.pdf');
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import { readFileSync } from 'node:fs';
|
|||||||
|
|
||||||
const indexHtml = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
const indexHtml = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
const viewerHtml = readFileSync(new URL('../viewer.html', import.meta.url), 'utf8');
|
const viewerHtml = readFileSync(new URL('../viewer.html', import.meta.url), 'utf8');
|
||||||
|
const repositoryCardsCss = readFileSync(new URL('../css/repository/cards.css', import.meta.url), 'utf8');
|
||||||
|
|
||||||
test('Tapops index uses module scripts and no inline behavior', () => {
|
test('Tapops index uses module scripts and no inline behavior', () => {
|
||||||
assert.match(indexHtml, /<script type="module" src="js\/app\.js"><\/script>/);
|
assert.match(indexHtml, /<script type="module" src="js\/app\.js"><\/script>/);
|
||||||
@@ -15,3 +16,7 @@ test('Tapops viewer uses module scripts and no inline behavior', () => {
|
|||||||
assert.match(viewerHtml, /<script type="module" src="js\/viewer\/viewer-app\.js"><\/script>/);
|
assert.match(viewerHtml, /<script type="module" src="js\/viewer\/viewer-app\.js"><\/script>/);
|
||||||
assert.doesNotMatch(viewerHtml, /<script>\s*\(/);
|
assert.doesNotMatch(viewerHtml, /<script>\s*\(/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Tapops document tags keep the API-provided casing', () => {
|
||||||
|
assert.doesNotMatch(repositoryCardsCss, /\.doc-tag\s*\{[^}]*text-transform:\s*lowercase/is);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user