components_share_dialog.js
/**
* Modal dialog for managing project presentation sharing.
* Allows owners to toggle "Anyone with link" access and copy the presentation URL.
*/
export class ShareDialog {
/**
* @param {string} projectId - The ID of the project to share.
* @param {object} server - Server instance (for token + baseUrl)
*/
constructor(projectId, server) {
this.projectId = projectId;
this.server = server;
this._anyWithLink = false;
this._buildDOM();
document.body.appendChild(this.root);
this._load();
}
/** Builds the dialog DOM and appends it to the document body. */
_buildDOM() {
// Scrim
this.scrim = document.createElement('div');
this.scrim.className = 'share-scrim';
this.scrim.addEventListener('click', () => this.close());
// Dialog
this.root = document.createElement('div');
this.root.className = 'share-dialog';
this.root.innerHTML = `
<div class="share-dialog-header">
<h2 class="share-dialog-title">Share Presentation</h2>
<button class="share-close-btn" title="Close">✕</button>
</div>
<div class="share-dialog-body">
<div class="share-section">
<div class="share-row">
<div class="share-row-text">
<span class="share-label">Anyone with link</span>
<span class="share-desc">Anyone who has this link can view the presentation without signing in.</span>
</div>
<label class="share-toggle">
<input type="checkbox" id="shareAnyWithLink" />
<span class="share-toggle-track"></span>
</label>
</div>
</div>
<div class="share-link-section" id="shareLinkSection" style="display:none">
<div class="share-link-label">Presentation link</div>
<div class="share-link-row">
<input class="share-link-input" id="shareLinkInput" type="text" readonly />
<button class="share-link-copy" id="shareLinkCopy">Copy</button>
</div>
</div>
<div class="share-actions">
<button class="share-open-btn" id="shareOpenBtn">Open Presentation</button>
</div>
</div>
<div class="share-status" id="shareStatus" style="display:none"></div>
`;
document.body.appendChild(this.scrim);
this.root.querySelector('.share-close-btn').addEventListener('click', () => this.close());
this._toggle = this.root.querySelector('#shareAnyWithLink');
this._linkSection = this.root.querySelector('#shareLinkSection');
this._linkInput = this.root.querySelector('#shareLinkInput');
this._copyBtn = this.root.querySelector('#shareLinkCopy');
this._openBtn = this.root.querySelector('#shareOpenBtn');
this._status = this.root.querySelector('#shareStatus');
const presUrl = `${window.location.origin}/presentation/${this.projectId}`;
this._linkInput.value = presUrl;
this._toggle.addEventListener('change', () => this._onToggle());
this._copyBtn.addEventListener('click', () => this._copyLink());
this._openBtn.addEventListener('click', () => window.open(presUrl, '_blank'));
}
/** Fetches the current sharing permissions from the server and updates the UI. */
async _load() {
try {
const token = await this.server.getToken();
const base = this.server.baseUrl;
const res = await fetch(`${base}/api/projects/${this.projectId}/permissions`, {
headers: token ? { 'X-Auth-Token': token } : {},
credentials: 'include',
});
if (!res.ok) throw new Error();
const perms = await res.json();
this._anyWithLink = !!perms.any_with_link;
} catch {
this._anyWithLink = false;
}
this._applyState();
}
/** Syncs the toggle and link-section visibility to the current `_anyWithLink` state. */
_applyState() {
this._toggle.checked = this._anyWithLink;
this._linkSection.style.display = this._anyWithLink ? '' : 'none';
}
/** Handles the "Anyone with link" toggle change: PUTs updated permissions to the server. */
async _onToggle() {
const newVal = this._toggle.checked;
this._toggle.disabled = true;
try {
const token = await this.server.getToken();
const base = this.server.baseUrl;
// GET current permissions first
const getRes = await fetch(`${base}/api/projects/${this.projectId}/permissions`, {
headers: token ? { 'X-Auth-Token': token } : {},
credentials: 'include',
});
const currentPerms = getRes.ok ? await getRes.json() : {};
// Merge change
const updated = { ...currentPerms, any_with_link: newVal };
const putRes = await fetch(`${base}/api/projects/${this.projectId}/permissions`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(token ? { 'X-Auth-Token': token } : {}),
},
credentials: 'include',
body: JSON.stringify({ permissions: updated }),
});
if (!putRes.ok) throw new Error();
this._anyWithLink = newVal;
this._applyState();
} catch {
// Revert toggle on failure
this._toggle.checked = this._anyWithLink;
this._showStatus('Could not update sharing settings.', true);
} finally {
this._toggle.disabled = false;
}
}
/** Copies the presentation link to the clipboard and briefly updates the button label. */
_copyLink() {
navigator.clipboard.writeText(this._linkInput.value).then(() => {
this._copyBtn.textContent = 'Copied!';
setTimeout(() => { this._copyBtn.textContent = 'Copy'; }, 2000);
});
}
/**
* Briefly displays a status message at the bottom of the dialog.
* @param {string} msg - The message to display.
* @param {boolean} [isError=false] - If true, renders the message in error colour.
*/
_showStatus(msg, isError = false) {
this._status.textContent = msg;
this._status.style.display = '';
this._status.style.color = isError ? 'var(--danger)' : 'var(--success)';
setTimeout(() => { this._status.style.display = 'none'; }, 3000);
}
/** Removes the dialog and scrim from the DOM. */
close() {
this.root.remove();
this.scrim.remove();
}
}