248 lines
13 KiB
PHP
248 lines
13 KiB
PHP
<?php
|
||
$csrfName = csrf_token();
|
||
$csrfHash = csrf_hash();
|
||
?>
|
||
<div class="modal fade" id="modalShare" tabindex="-1" aria-hidden="true" data-csrf-name="<?= esc($csrfName) ?>" data-csrf-hash="<?= esc($csrfHash) ?>">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<div class="modal-content border-0 shadow-lg rounded-4">
|
||
<div class="modal-header border-0">
|
||
<h2 class="modal-title h5 fw-bold">Share</h2>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabShareLink" type="button">Public link</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabShareEmbed" type="button">Embed</button>
|
||
</li>
|
||
</ul>
|
||
<div class="tab-content">
|
||
<div class="tab-pane fade show active" id="tabShareLink">
|
||
<form id="cbShareGenerateForm" class="row g-2 mb-3">
|
||
<input type="hidden" name="<?= esc($csrfName) ?>" value="<?= esc($csrfHash) ?>" class="cb-share-csrf">
|
||
<input type="hidden" name="type" id="cbShareType" value="chart">
|
||
<input type="hidden" name="resource_id" id="cbShareResourceId" value="">
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Optional password</label>
|
||
<input type="password" name="password" class="form-control form-control-sm" autocomplete="new-password" placeholder="Leave empty for open access">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Optional expiry</label>
|
||
<input type="datetime-local" name="expires_at" class="form-control form-control-sm">
|
||
</div>
|
||
<div class="col-12">
|
||
<button type="submit" class="btn btn-dark btn-sm">Generate new link</button>
|
||
</div>
|
||
</form>
|
||
<p class="text-danger small d-none" id="cbShareErr"></p>
|
||
<div id="cbShareLinksWrap" class="small">
|
||
<p class="text-muted mb-0">Open Share to load existing links.</p>
|
||
</div>
|
||
</div>
|
||
<div class="tab-pane fade" id="tabShareEmbed">
|
||
<label class="form-label small">Preset size</label>
|
||
<select class="form-select form-select-sm mb-2" id="cbEmbedPreset">
|
||
<option value="640x480">640 × 480</option>
|
||
<option value="800x600">800 × 600</option>
|
||
<option value="1200x800" selected>1200 × 800</option>
|
||
<option value="custom">Custom</option>
|
||
</select>
|
||
<div class="row g-2 mb-2 d-none" id="cbEmbedCustomDims">
|
||
<div class="col-6">
|
||
<input type="number" class="form-control form-control-sm" id="cbEmbedW" value="1200" min="200" max="2400">
|
||
</div>
|
||
<div class="col-6">
|
||
<input type="number" class="form-control form-control-sm" id="cbEmbedH" value="800" min="200" max="1600">
|
||
</div>
|
||
</div>
|
||
<label class="form-label small">Iframe HTML</label>
|
||
<textarea class="form-control font-monospace small" id="cbEmbedCode" rows="4" readonly></textarea>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" id="cbEmbedCopy">Copy embed code</button>
|
||
<p class="text-muted small mt-2 mb-0">Uses <code>?embed=1</code> and allows embedding in external pages.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
const modal = document.getElementById('modalShare');
|
||
if (!modal) return;
|
||
const urlGen = <?= json_encode(rtrim(base_url(), '/') . '/sharing/generate') ?>;
|
||
const urlList = <?= json_encode(rtrim(base_url(), '/') . '/sharing/links') ?>;
|
||
const urlRevoke = <?= json_encode(rtrim(base_url(), '/') . '/sharing/revoke/') ?>;
|
||
|
||
function getCsrf() {
|
||
return { name: modal.getAttribute('data-csrf-name') || '', hash: modal.querySelector('.cb-share-csrf')?.value || '' };
|
||
}
|
||
|
||
function setEmbedSnippet(url) {
|
||
const preset = document.getElementById('cbEmbedPreset')?.value || '1200x800';
|
||
let w = 1200, h = 800;
|
||
if (preset === 'custom') {
|
||
w = parseInt(document.getElementById('cbEmbedW')?.value || '1200', 10);
|
||
h = parseInt(document.getElementById('cbEmbedH')?.value || '800', 10);
|
||
} else {
|
||
const p = preset.split('x');
|
||
w = parseInt(p[0], 10) || 1200;
|
||
h = parseInt(p[1], 10) || 800;
|
||
}
|
||
const u = (url || '').split('#')[0];
|
||
const src = u.indexOf('?') >= 0 ? u + '&embed=1' : u + '?embed=1';
|
||
const code = '<iframe src="' + src.replace(/"/g, '"') + '" width="' + w + '" height="' + h + '" style="border:0;" loading="lazy" title="Chart-Board"></iframe>';
|
||
const ta = document.getElementById('cbEmbedCode');
|
||
if (ta) ta.value = code;
|
||
}
|
||
|
||
async function loadLinks() {
|
||
const type = document.getElementById('cbShareType')?.value || 'chart';
|
||
const rid = document.getElementById('cbShareResourceId')?.value || '';
|
||
const wrap = document.getElementById('cbShareLinksWrap');
|
||
const err = document.getElementById('cbShareErr');
|
||
if (!wrap || !rid) return;
|
||
err.classList.add('d-none');
|
||
wrap.innerHTML = '<p class="text-muted mb-0">Loading…</p>';
|
||
try {
|
||
const r = await fetch(urlList + '?type=' + encodeURIComponent(type) + '&resource_id=' + encodeURIComponent(rid), {
|
||
credentials: 'same-origin',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||
});
|
||
const j = await r.json();
|
||
if (!j.success || !j.links) {
|
||
wrap.innerHTML = '<p class="text-muted mb-0">No links yet.</p>';
|
||
return;
|
||
}
|
||
if (j.links.length === 0) {
|
||
wrap.innerHTML = '<p class="text-muted mb-0">No active links. Generate one above.</p>';
|
||
setEmbedSnippet('');
|
||
return;
|
||
}
|
||
let html = '<ul class="list-group list-group-flush">';
|
||
j.links.forEach((L) => {
|
||
const qr = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=' + encodeURIComponent(L.url);
|
||
html += '<li class="list-group-item px-0">' +
|
||
'<div class="d-flex flex-wrap gap-2 align-items-start">' +
|
||
'<div class="flex-grow-1 min-w-0">' +
|
||
'<div class="input-group input-group-sm mb-1">' +
|
||
'<input type="text" class="form-control font-monospace small" readonly value="' + String(L.url).replace(/"/g, '"') + '">' +
|
||
'<button type="button" class="btn btn-outline-secondary cb-copy-url">Copy</button></div>' +
|
||
'<div class="text-muted" style="font-size:11px;">Views: ' + (L.view_count ?? 0) +
|
||
(L.expires_at ? ' · Expires: ' + L.expires_at : '') +
|
||
(L.has_password ? ' · Password protected' : '') + '</div>' +
|
||
'<form class="d-inline mt-1 cb-revoke-form" data-id="' + L.id + '">' +
|
||
'<button type="submit" class="btn btn-link btn-sm text-danger p-0">Revoke</button></form>' +
|
||
'</div><div><img src="' + qr + '" width="120" height="120" alt="QR code"></div></div></li>';
|
||
});
|
||
html += '</ul>';
|
||
wrap.innerHTML = html;
|
||
setEmbedSnippet(j.links[0].url);
|
||
wrap.querySelectorAll('.cb-copy-url').forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
const inp = btn.closest('.input-group')?.querySelector('input');
|
||
if (!inp) return;
|
||
inp.select();
|
||
document.execCommand('copy');
|
||
});
|
||
});
|
||
wrap.querySelectorAll('.cb-revoke-form').forEach((f) => {
|
||
f.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault();
|
||
const id = f.getAttribute('data-id');
|
||
const csrf = getCsrf();
|
||
const body = new URLSearchParams();
|
||
body.append(csrf.name, csrf.hash);
|
||
const res = await fetch(urlRevoke + id, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body,
|
||
});
|
||
const jj = await res.json();
|
||
if (jj.csrf) {
|
||
modal.setAttribute('data-csrf-name', jj.csrf.name);
|
||
const hi = modal.querySelector('.cb-share-csrf');
|
||
if (hi) { hi.name = jj.csrf.name; hi.value = jj.csrf.hash; }
|
||
}
|
||
loadLinks();
|
||
});
|
||
});
|
||
} catch (e) {
|
||
wrap.innerHTML = '<p class="text-danger mb-0">Could not load links.</p>';
|
||
}
|
||
}
|
||
|
||
modal.addEventListener('show.bs.modal', (ev) => {
|
||
const t = ev.relatedTarget;
|
||
const type = t?.getAttribute('data-share-type') || 'chart';
|
||
const rid = t?.getAttribute('data-resource-id') || '';
|
||
const st = document.getElementById('cbShareType');
|
||
const sr = document.getElementById('cbShareResourceId');
|
||
if (st) st.value = type;
|
||
if (sr) sr.value = rid;
|
||
loadLinks();
|
||
});
|
||
|
||
document.getElementById('cbShareGenerateForm')?.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault();
|
||
const err = document.getElementById('cbShareErr');
|
||
err.classList.add('d-none');
|
||
const form = ev.target;
|
||
const fd = new FormData(form);
|
||
try {
|
||
const r = await fetch(urlGen, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||
body: new URLSearchParams(fd),
|
||
});
|
||
const j = await r.json();
|
||
if (j.csrf) {
|
||
const hi = modal.querySelector('.cb-share-csrf');
|
||
if (hi) { hi.name = j.csrf.name; hi.value = j.csrf.hash; }
|
||
}
|
||
if (!j.success) {
|
||
err.textContent = j.message || (j.errors ? Object.values(j.errors).flat().join(' ') : 'Failed');
|
||
err.classList.remove('d-none');
|
||
return;
|
||
}
|
||
if (j.csrf) {
|
||
const hi = modal.querySelector('.cb-share-csrf');
|
||
if (hi) { hi.name = j.csrf.name; hi.value = j.csrf.hash; }
|
||
modal.setAttribute('data-csrf-name', j.csrf.name);
|
||
}
|
||
form.querySelector('[name="password"]').value = '';
|
||
form.querySelector('[name="expires_at"]').value = '';
|
||
loadLinks();
|
||
} catch (e) {
|
||
err.textContent = 'Request failed.';
|
||
err.classList.remove('d-none');
|
||
}
|
||
});
|
||
|
||
document.getElementById('cbEmbedPreset')?.addEventListener('change', () => {
|
||
const c = document.getElementById('cbEmbedCustomDims');
|
||
if (c) c.classList.toggle('d-none', document.getElementById('cbEmbedPreset').value !== 'custom');
|
||
const first = document.querySelector('#cbShareLinksWrap input[readonly]');
|
||
setEmbedSnippet(first ? first.value : '');
|
||
});
|
||
document.getElementById('cbEmbedW')?.addEventListener('input', () => {
|
||
const first = document.querySelector('#cbShareLinksWrap input[readonly]');
|
||
setEmbedSnippet(first ? first.value : '');
|
||
});
|
||
document.getElementById('cbEmbedH')?.addEventListener('input', () => {
|
||
const first = document.querySelector('#cbShareLinksWrap input[readonly]');
|
||
setEmbedSnippet(first ? first.value : '');
|
||
});
|
||
|
||
document.getElementById('cbEmbedCopy')?.addEventListener('click', () => {
|
||
const ta = document.getElementById('cbEmbedCode');
|
||
if (!ta) return;
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
});
|
||
})();
|
||
</script>
|