mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-01-23 02:35:34 +00:00
7139 let user maintain a single session across multiple browsers (#7228)
* chore: started with implementation * chore: finished index page * chore: started with double sided modal * chore: continue * chore: completed implementation of transfer token * chore: fixed typescript checks
This commit is contained in:
parent
658ae78922
commit
41cb6803d2
10 changed files with 327 additions and 16 deletions
|
|
@ -58,6 +58,12 @@
|
|||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "transferToken",
|
||||
"hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/tokenTransfer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pwa",
|
||||
"hooks": {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,19 @@
|
|||
"admin_settings.page-title": "Einstellungen - Etherpad",
|
||||
"index.newPad": "Neues Pad",
|
||||
"index.createOpenPad": "Pad öffnen",
|
||||
"index.settings": "Einstellungen",
|
||||
"index.receiveSessionTitle": "Sitzung empfangen",
|
||||
"index.receiveSessionDescription": "Hier kannst du eine Etherpad-Sitzung aus einem anderen Browser oder Gerät empfangen. Bedenke allerdings, dass dadurch deine aktuelle Sitzung, falls vorhanden gelöscht wird.",
|
||||
"index.code": "Übertragungscode",
|
||||
"index.transferSessionTitle": "Sitzung übertragen",
|
||||
"index.transferSession": "1. Sitzung übertragen",
|
||||
"index.copyLink": "2. Link kopieren",
|
||||
"index.copyLinkButton": "Übertragungscode kopieren",
|
||||
"index.copyLinkDescription": "Klicke auf den untenstehenden Button, um den Übertragungscode in deine Zwischenablage zu kopieren.",
|
||||
"index.transferToSystem": "3. Sitzung einfügen",
|
||||
"index.transferToSystemDescription": "Öffne den kopierten Link in dem neuen Browser oder Gerät, um deine aktuelle Etherpad-Sitzung zu übertragen.",
|
||||
"index.transferSessionNow": "Jetzt übertragen",
|
||||
"index.transferSessionDescription": "Übertrage deine aktuelle Etherpad-Sitzung zu einem anderen Browser oder Gerät, indem du den untenstehenden Button klickst. Dabei wird ein Link in deine Zwischenablage kopiert, den du im neuen Browser oder Gerät öffnen kannst, um deine Sitzung zu übertragen.",
|
||||
"index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:",
|
||||
"index.recentPads": "Zuletzt bearbeitete Pads",
|
||||
"index.recentPadsEmpty": "Keine kürzlich bearbeiteten Pads gefunden.",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@
|
|||
"admin_settings.page-title": "Settings - Etherpad",
|
||||
|
||||
"index.newPad": "New Pad",
|
||||
"index.settings": "Settings",
|
||||
"index.transferSessionTitle": "Transfer session",
|
||||
"index.receiveSessionTitle": "Receive session",
|
||||
"index.receiveSessionDescription": "Here you can receive an Etherpad session from another browser or device. Please note, however, that this will delete your current session, if any.",
|
||||
"index.transferSession": "1. Transfer session",
|
||||
"index.transferSessionNow": "Transfer session now",
|
||||
"index.copyLink": "2. Copy link",
|
||||
"index.copyLinkDescription": "Click on the button below to copy the link to your clipboard.",
|
||||
"index.copyLinkButton": "Copy link to clipboard",
|
||||
"index.transferToSystem": "3. Copy session to new system",
|
||||
"index.transferToSystemDescription": "Open the copied link in the target browser or device to transfer your session.",
|
||||
"index.transferSessionDescription": "Transfer your current session to browser or device by clicking the button below. This will copy a link to a page that will transfer your session when opened in the target browser or device.",
|
||||
"index.createOpenPad": "Open pad by name",
|
||||
"index.openPad": "open an existing Pad with the name:",
|
||||
"index.recentPads": "Recent Pads",
|
||||
|
|
|
|||
45
src/node/hooks/express/tokenTransfer.ts
Normal file
45
src/node/hooks/express/tokenTransfer.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
const db = require('../../db/DB');
|
||||
import crypto from 'crypto'
|
||||
|
||||
|
||||
type TokenTransferRequest = {
|
||||
token: string;
|
||||
prefsHttp: string,
|
||||
createdAt?: number;
|
||||
}
|
||||
|
||||
const tokenTransferKey = "tokenTransfer:";
|
||||
|
||||
export const expressCreateServer = (hookName:string, {app}:ArgsExpressType) => {
|
||||
app.post('/tokenTransfer', async (req, res) => {
|
||||
const token = req.body as TokenTransferRequest;
|
||||
if (!token || !token.token) {
|
||||
return res.status(400).send({error: 'Invalid request'});
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID()
|
||||
token.createdAt = Date.now();
|
||||
|
||||
await db.set(`${tokenTransferKey}:${id}`, token)
|
||||
res.send({id});
|
||||
})
|
||||
|
||||
app.get('/tokenTransfer/:token', async (req, res) => {
|
||||
const id = req.params.token;
|
||||
if (!id) {
|
||||
return res.status(400).send({error: 'Invalid request'});
|
||||
}
|
||||
|
||||
const tokenData = await db.get(`${tokenTransferKey}:${id}`);
|
||||
if (!tokenData) {
|
||||
return res.status(404).send({error: 'Token not found'});
|
||||
}
|
||||
|
||||
const token = await db.get(`${tokenTransferKey}:${id}`)
|
||||
|
||||
res.cookie('token', tokenData.token, {path: '/', maxAge: 1000*60*60*24*365});
|
||||
res.cookie('prefsHttp', tokenData.prefsHttp, {path: '/', maxAge: 1000*60*60*24*365});
|
||||
res.send(token);
|
||||
})
|
||||
}
|
||||
116
src/static/js/welcome.ts
Normal file
116
src/static/js/welcome.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
const checkmark = '<svg width="28" height="28" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor"><path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>';
|
||||
|
||||
function getCookie(name: string) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) { // @ts-ignore
|
||||
return parts.pop().split(';').shift();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleTransferOfSession() {
|
||||
const transferNowButton = document.querySelector('[data-l10n-id="index.transferSessionNow"]')! as HTMLButtonElement;
|
||||
|
||||
transferNowButton.addEventListener('click', async () => {
|
||||
transferNowButton.style.display = 'inline-flex';
|
||||
transferNowButton.style.alignItems = 'center';
|
||||
transferNowButton.style.justifyContent = 'center';
|
||||
transferNowButton.innerHTML = `${checkmark}`;
|
||||
transferNowButton.disabled = true;
|
||||
|
||||
const responseWithId = await fetch("./tokenTransfer", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prefsHttp: getCookie('prefsHttp'),
|
||||
token: getCookie('token'),
|
||||
})
|
||||
})
|
||||
|
||||
const copyLinkSection = document.getElementById('copy-link-section')
|
||||
if (!copyLinkSection) return;
|
||||
copyLinkSection.style.display = 'block';
|
||||
|
||||
const copyButton = document.querySelector('#copy-link-section .btn-secondary') as HTMLButtonElement
|
||||
const responseData = await responseWithId.json();
|
||||
copyButton.addEventListener('click', async ()=>{
|
||||
await navigator.clipboard.writeText(responseData.id);
|
||||
copyButton.style.display = 'inline-flex';
|
||||
copyButton.style.alignItems = 'center';
|
||||
copyButton.style.justifyContent = 'center';
|
||||
copyButton.innerHTML = `${checkmark}`;
|
||||
copyButton.disabled = true;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const handleSettingsButtonClick = () => {
|
||||
const settingsButton = document.querySelector('.settings-button')!;
|
||||
const settingsDialog = document.getElementById('settings-dialog') as HTMLDialogElement;
|
||||
let initialSettingsHtml: string;
|
||||
|
||||
settingsDialog.addEventListener('click', (e) => {
|
||||
if (e.target === settingsDialog) {
|
||||
settingsDialog.close();
|
||||
settingsDialog.innerHTML = initialSettingsHtml;
|
||||
handleMenuBarClicked();
|
||||
handleTransferOfSession();
|
||||
}
|
||||
});
|
||||
|
||||
settingsButton.addEventListener('click', () => {
|
||||
initialSettingsHtml = settingsDialog.innerHTML;
|
||||
settingsDialog.showModal();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleMenuBarClicked = () => {
|
||||
const menuBar = document.getElementById('button-bar')!;
|
||||
menuBar.querySelectorAll('button').forEach((button, index)=>{
|
||||
button.addEventListener('click', ()=>{
|
||||
menuBar.querySelectorAll('button').forEach((btn)=>btn.classList.remove('active-btn'));
|
||||
button.classList.add('active-btn');
|
||||
|
||||
const sections: NodeListOf<HTMLDivElement> = document.querySelectorAll('#settings-dialog > div');
|
||||
sections.forEach((section, index)=>index >= 1 && (section.style.display = 'none'));
|
||||
(sections[index +1] as HTMLElement).style.display = 'block';
|
||||
});
|
||||
})
|
||||
|
||||
const transferSessionButton = document.getElementById('transferSessionButton')
|
||||
const codeInputField = document.getElementById('codeInput') as HTMLInputElement
|
||||
if (transferSessionButton) {
|
||||
transferSessionButton.addEventListener('click', ()=>{
|
||||
const code = codeInputField.value
|
||||
fetch("./tokenTransfer/"+code, {
|
||||
method: 'GET'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(()=>{
|
||||
window.location.reload()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (codeInputField) {
|
||||
codeInputField.addEventListener('input', (e)=>{
|
||||
if ((e.target as HTMLInputElement).value?.length === 36) {
|
||||
transferSessionButton?.removeAttribute('disabled');
|
||||
} else {
|
||||
transferSessionButton?.setAttribute('disabled', 'true');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
handleSettingsButtonClick();
|
||||
handleMenuBarClicked();
|
||||
handleTransferOfSession();
|
||||
});
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@import url("./src/components/buttons.css");
|
||||
|
||||
:root {
|
||||
--etherpad-color: #64d29b;
|
||||
--etherpad-color-dark: #4a5d5c;
|
||||
|
|
@ -100,7 +102,7 @@ h1 {
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#button, #button:hover, #go2Name [type="submit"] {
|
||||
#button, #button:hover, #go2Name [type="submit"], #transferSessionButton {
|
||||
order: 2;
|
||||
margin-top: 0.5rem;
|
||||
line-height: 1.25rem;
|
||||
|
|
@ -115,10 +117,14 @@ h1 {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#go2Name [type="submit"]:hover {
|
||||
#go2Name [type="submit"]:hover, #transferSessionButton {
|
||||
background-color: oklch(52.7% 0.154 150.069)
|
||||
}
|
||||
|
||||
#transferSessionButton:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#button, #button:hover {
|
||||
order: 2;
|
||||
}
|
||||
|
|
@ -132,7 +138,7 @@ h1 {
|
|||
}
|
||||
|
||||
|
||||
#go2Name [type="submit"] {
|
||||
#go2Name [type="submit"], #transferSessionButton {
|
||||
display: block;
|
||||
background-color: var(--ep-color);
|
||||
color: white;
|
||||
|
|
@ -234,10 +240,25 @@ a, a:visited, a:hover, a:active {
|
|||
border-bottom-color: #e5e7eb;
|
||||
}
|
||||
|
||||
#settings-dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
#codeInput {
|
||||
height: auto;
|
||||
position: static;
|
||||
border: 1px solid var(--muted-border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#inner {
|
||||
max-width: 100%;
|
||||
|
|
|
|||
|
|
@ -11,24 +11,25 @@ window.addEventListener('pageshow', (event) => {
|
|||
});
|
||||
|
||||
window.customStart = () => {
|
||||
document.getElementById('recent-pads').replaceChildren()
|
||||
const recentPadList = document.getElementById('recent-pads');
|
||||
if (recentPadList) {
|
||||
recentPadList.replaceChildren();
|
||||
}
|
||||
// define your javascript here
|
||||
// jquery is available - except index.js
|
||||
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
|
||||
const divHoldingPlaceHolderLabel = document
|
||||
.querySelector('[data-l10n-id="index.placeholderPadEnter"]');
|
||||
.querySelector('[data-l10n-id="index.placeholderPadEnter"]');
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
document.querySelector('#go2Name input')
|
||||
.setAttribute('placeholder', divHoldingPlaceHolderLabel.textContent);
|
||||
.setAttribute('placeholder', divHoldingPlaceHolderLabel.textContent);
|
||||
});
|
||||
|
||||
observer
|
||||
.observe(divHoldingPlaceHolderLabel, {childList: true, subtree: true, characterData: true});
|
||||
.observe(divHoldingPlaceHolderLabel, {childList: true, subtree: true, characterData: true});
|
||||
|
||||
|
||||
const recentPadList = document.getElementById('recent-pads');
|
||||
const parentStyle = recentPadList.parentElement.style;
|
||||
const recentPadListHeading = document.querySelector('[data-l10n-id="index.recentPads"]');
|
||||
const recentPadsFromLocalStorage = localStorage.getItem('recentPads');
|
||||
let recentPadListData = [];
|
||||
|
|
@ -38,18 +39,18 @@ window.customStart = () => {
|
|||
|
||||
// Remove duplicates based on pad name and sort by timestamp
|
||||
recentPadListData = recentPadListData.filter(
|
||||
(pad, index, self) =>
|
||||
index === self.findIndex((p) => p.name === pad.name)
|
||||
(pad, index, self) => index === self.findIndex((p) => p.name === pad.name)
|
||||
).sort((a, b) => new Date(a.timestamp) > new Date(b.timestamp) ? -1 : 1);
|
||||
|
||||
if (recentPadListData.length === 0) {
|
||||
if (recentPadList && recentPadListData.length === 0) {
|
||||
const parentStyle = recentPadList.parentElement.style;
|
||||
recentPadListHeading.setAttribute('data-l10n-id', 'index.recentPadsEmpty');
|
||||
parentStyle.display = 'flex';
|
||||
parentStyle.justifyContent = 'center';
|
||||
parentStyle.alignItems = 'center';
|
||||
parentStyle.maxHeight = '100%';
|
||||
recentPadList.remove();
|
||||
} else {
|
||||
} else if (recentPadList) {
|
||||
/**
|
||||
* @typedef {Object} Pad
|
||||
* @property {string} name
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ button, .btn
|
|||
width: auto;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
|
@ -23,3 +22,36 @@ button, .btn
|
|||
color: #485365;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Sekundär (outlined) */
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #1f8a3e;
|
||||
border: 2px solid #1f8a3e;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.active-btn {
|
||||
text-underline-offset: 10px;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: #1f8a3e;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #1f8a3e;
|
||||
color: #fff;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 2px rgba(0,0,0,0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
background: transparent;
|
||||
color: #aaa;
|
||||
border-color: #aaa;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
#padname{
|
||||
max-width:280px;
|
||||
}
|
||||
form {
|
||||
#go2Name {
|
||||
height: 38px;
|
||||
background: #fff;
|
||||
border: 1px solid #bbb;
|
||||
|
|
@ -109,13 +109,32 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 800px) {
|
||||
.settings-button {
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
#settings-dialog {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (min-device-width: 320px) and (min-device-width: 800px) {
|
||||
body {
|
||||
background: #bbb;
|
||||
background: -webkit-linear-gradient(#aaa,#eee 60%) center fixed;
|
||||
background: -moz-linear-gradient(#aaa,#eee 60%) center fixed;
|
||||
background: -ms-linear-gradient(#aaa,#eee 60%) center fixed;
|
||||
}
|
||||
#settings-dialog {
|
||||
max-width: 50%;
|
||||
}
|
||||
#wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -136,9 +155,54 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text w-5 h-5 text-white"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path><path d="M14 2v4a2 2 0 0 0 2 2h4"></path><path d="M10 9H8"></path><path d="M16 13H8"></path><path d="M16 17H8"></path></svg>
|
||||
</div>
|
||||
<h1>Etherpad</h1>
|
||||
<div style="flex-grow: 1"></div>
|
||||
<button class="settings-button" aria-label="Settings">
|
||||
<svg width="30px" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="settings-icon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Settings menu-->
|
||||
<dialog id="settings-dialog">
|
||||
<div id="button-bar">
|
||||
<button data-l10n-id="index.transferSessionTitle" class="active-btn"></button>
|
||||
<button data-l10n-id="index.receiveSessionTitle"></button>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<!-- Initial link button -->
|
||||
<h3 data-l10n-id="index.transferSession"></h3>
|
||||
<div data-l10n-id="index.transferSessionDescription"></div>
|
||||
<button type="button" class="btn-secondary" style="margin-top: 20px" data-l10n-id="index.transferSessionNow"></button>
|
||||
|
||||
<!-- Copy link button -->
|
||||
<div style="display: none" id="copy-link-section">
|
||||
<h3 data-l10n-id="index.copyLink"></h3>
|
||||
<div data-l10n-id="index.copyLinkDescription"></div>
|
||||
<button type="button" class="btn-secondary" style="margin-top: 20px" data-l10n-id="index.copyLinkButton"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transfer-to-system-section" style="display: none; margin-top: 30px;">
|
||||
<h3 data-l10n-id="index.transferToSystem"></h3>
|
||||
<div data-l10n-id="index.transferToSystemDescription"></div>
|
||||
<p data-l10n-id="index.receiveSessionDescription"></p>
|
||||
<div>
|
||||
<label for="codeInput" data-l10n-id="index.code"></label>
|
||||
<input type="text" id="codeInput"/>
|
||||
</div>
|
||||
|
||||
<button data-l10n-id="index.transferSessionTitle" id="transferSessionButton" disabled></button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
|
||||
<div class="body">
|
||||
<div class="mission-statement">
|
||||
<h2 data-l10n-id="index.createAndShareDocuments"></h2>
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
|
||||
require('ep_etherpad-lite/static/js/l10n')
|
||||
require('ep_etherpad-lite/static/js/index')
|
||||
require('ep_etherpad-lite/static/js/welcome')
|
||||
})()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue