merge master
commit
1092285a68
@ -0,0 +1,7 @@
|
|||||||
|
[ignore]
|
||||||
|
|
||||||
|
[include]
|
||||||
|
|
||||||
|
[libs]
|
||||||
|
|
||||||
|
[options]
|
@ -0,0 +1,371 @@
|
|||||||
|
define(function () {
|
||||||
|
var out = {};
|
||||||
|
|
||||||
|
out.main_title = "CryptPad: Zero Knowledge, Colaborare în timp real";
|
||||||
|
out.main_slogan = "Puterea stă în cooperare - Colaborarea este cheia";
|
||||||
|
|
||||||
|
out.type = {};
|
||||||
|
out.pad = "Rich text";
|
||||||
|
out.code = "Code";
|
||||||
|
out.poll = "Poll";
|
||||||
|
out.slide = "Presentation";
|
||||||
|
out.drive = "Drive";
|
||||||
|
out.whiteboard = "Whiteboard";
|
||||||
|
out.file = "File";
|
||||||
|
out.media = "Media";
|
||||||
|
|
||||||
|
out.button_newpad = "Filă Text Nouă";
|
||||||
|
out.button_newcode = "Filă Cod Nouă";
|
||||||
|
out.button_newpoll = "Sondaj Nou";
|
||||||
|
out.button_newslide = "Prezentare Nouă";
|
||||||
|
out.button_newwhiteboard = "Fila Desen Nouă";
|
||||||
|
out.updated_0_common_connectionLost = "<b>Conexiunea la server este pierdută</b><br>Până la revenirea conexiunii, vei fi în modul citire";
|
||||||
|
out.common_connectionLost = out.updated_0_common_connectionLost;
|
||||||
|
out.websocketError = "Conexiune inexistentă către serverul websocket...";
|
||||||
|
out.typeError = "Această filă nu este compatibilă cu aplicația aleasă";
|
||||||
|
out.onLogout = "Nu mai ești autentificat, <a href=\"/\" target=\"_blank\">apasă aici</a> să te autentifici<br>sau apasă <em>Escape</em>să accesezi fila în modul citire.";
|
||||||
|
out.wrongApp = "Momentan nu putem arăta conținutul sesiunii în timp real în fereastra ta. Te rugăm reîncarcă pagina.";
|
||||||
|
out.loading = "Încarcă...";
|
||||||
|
out.error = "Eroare";
|
||||||
|
|
||||||
|
out.saved = "Salvat";
|
||||||
|
out.synced = "Totul a fost salvat";
|
||||||
|
out.deleted = "Pad șters din CryptDrive-ul tău";
|
||||||
|
out.disconnected = "Deconectat";
|
||||||
|
out.synchronizing = "Se sincronizează";
|
||||||
|
out.reconnecting = "Reconectare...";
|
||||||
|
out.lag = "Decalaj";
|
||||||
|
out.readonly = "Mod citire";
|
||||||
|
out.anonymous = "Anonim";
|
||||||
|
out.yourself = "Tu";
|
||||||
|
out.anonymousUsers = "editori anonimi";
|
||||||
|
out.anonymousUser = "editor anonim";
|
||||||
|
out.users = "Utilizatori";
|
||||||
|
out.and = "Și";
|
||||||
|
out.viewer = "privitor";
|
||||||
|
out.viewers = "privitori";
|
||||||
|
out.editor = "editor";
|
||||||
|
out.editors = "editori";
|
||||||
|
out.language = "Limbă";
|
||||||
|
out.upgrade = "Actualizare";
|
||||||
|
out.upgradeTitle = "Actualizează-ți contul pentru a mări limita de stocare";
|
||||||
|
out.MB = "MB";
|
||||||
|
out.greenLight = "Totul funcționează corespunzător";
|
||||||
|
out.orangeLight = "Conexiunea lentă la internet îți poate afecta experiența";
|
||||||
|
out.redLight = "Ai fost deconectat de la sesiune";
|
||||||
|
out.pinLimitReached = "Ai atins limita de stocare";
|
||||||
|
out.pinLimitReachedAlert = "Ai atins limita de stocare. Noile pad-uri nu vor mai fi stocate în CryptDrive.<br>Pentru a rezolva această problemă, poți să nlături pad-uri din CryptDrive-ul tău (incluzând gunoiul) sau să subscrii la un pachet premium pentru a-ți extinde spațiul de stocare.";
|
||||||
|
out.pinLimitNotPinned = "Ai atins limita de stocare.<br>Acest pad nu va fi stocat n CryptDrive-ul tău.";
|
||||||
|
out.pinLimitDrive = "Ai atins limita de stocare.<br>Nu poți să creezi alte pad-uri.";
|
||||||
|
out.importButtonTitle = "Importă un pad dintr-un fișier local";
|
||||||
|
out.exportButtonTitle = "Exportă pad-ul acesta către un fișier local";
|
||||||
|
out.exportPrompt = "Cum ai vrea să îți denumești fișierul?";
|
||||||
|
out.changeNamePrompt = "Schimbă-ți numele (lasă necompletat dacă vrei să fii anonim): ";
|
||||||
|
out.user_rename = "Schimbă numele afișat";
|
||||||
|
out.user_displayName = "Nume afișat";
|
||||||
|
out.user_accountName = "Nume cont";
|
||||||
|
out.clickToEdit = "Click pentru editare";
|
||||||
|
out.forgetButtonTitle = "Mută acest pad la gunoi";
|
||||||
|
out.forgetPrompt = "Click-ul pe OK va muta acest pad la gunoi. Ești sigur?";
|
||||||
|
out.movedToTrash = "Acest pad a fost mutat la gunoi.<br><a href=\"/drive/\">Acesează-mi Drive-ul</a>";
|
||||||
|
out.shareButton = "Distribuie";
|
||||||
|
out.shareSuccess = "Link copiat în clipboard";
|
||||||
|
out.newButton = "Nou";
|
||||||
|
out.newButtonTitle = "Crează un nou pad";
|
||||||
|
out.saveTemplateButton = "Salvează ca șablon";
|
||||||
|
out.saveTemplatePrompt = "Alege un titlu pentru șablon";
|
||||||
|
out.templateSaved = "Șablon salvat!";
|
||||||
|
out.selectTemplate = "Selectează un șablon sau apasă escape";
|
||||||
|
out.presentButtonTitle = "Intră în modul de prezentare";
|
||||||
|
out.presentSuccess = "Apasă ESC pentru a ieși din modul de prezentare";
|
||||||
|
out.backgroundButtonTitle = "Schimbă culoarea de fundal din prezentare";
|
||||||
|
out.colorButtonTitle = "Schimbă culoarea textului în modul de prezentare";
|
||||||
|
out.printButton = "Printează (enter)";
|
||||||
|
out.printButtonTitle = "Printează-ți slide-urile sau exportă-le ca fișier PDF";
|
||||||
|
out.printOptions = "Opțiuni schemă";
|
||||||
|
out.printSlideNumber = "Afișează numărul slide-ului";
|
||||||
|
out.printDate = "Afișează data";
|
||||||
|
out.printTitle = "Afișează titlul pad-ului";
|
||||||
|
out.printCSS = "Reguli de stil personalizate (CSS):";
|
||||||
|
out.printTransition = "Permite tranziția animațiilor";
|
||||||
|
out.slideOptionsTitle = "Personalizează-ți slide-urile";
|
||||||
|
out.slideOptionsButton = "Salvează (enter)";
|
||||||
|
out.editShare = "Editează link-ul";
|
||||||
|
out.editShareTitle = "Copiază link-ul de editare în clipboard";
|
||||||
|
out.editOpen = "Deschide link-ul de editare într-o nouă filă";
|
||||||
|
out.editOpenTitle = "Deschide acest pad în modul de editare într-o nouă filă";
|
||||||
|
out.viewShare = "Link în modul citire";
|
||||||
|
out.viewShareTitle = "Copiază link-ul în modul de citire în clipboard";
|
||||||
|
out.viewOpen = "Deschide link-ul în modul de citire într-o filă nouă";
|
||||||
|
out.viewOpenTitle = "Deschide acest pad în modul de citire într-o nouă filă";
|
||||||
|
out.notifyJoined = "{0} s-au alăturat sesiunii colaborative";
|
||||||
|
out.notifyRenamed = "{0} e cunoscut ca {1}";
|
||||||
|
out.notifyLeft = "{0} au părăsit sesiunea colaborativă";
|
||||||
|
out.okButton = "OK (enter)";
|
||||||
|
out.cancel = "Anulează";
|
||||||
|
out.cancelButton = "Anulează (esc)";
|
||||||
|
out.historyButton = "Afișează istoricul documentului";
|
||||||
|
out.history_next = "Mergi la versiunea următoare";
|
||||||
|
out.history_prev = "Mergi la versiunea trecută";
|
||||||
|
out.history_goTo = "Mergi la sesiunea selectată";
|
||||||
|
out.history_close = "Înapoi";
|
||||||
|
out.history_closeTitle = "Închide istoricul";
|
||||||
|
out.history_restore = "Restabilește";
|
||||||
|
out.history_restoreTitle = "Restabilește versiunea selectată a documentului";
|
||||||
|
out.history_restorePrompt = "Ești sigur că vrei să înlocuiești versiunea curentă a documentului cu cea afișată?";
|
||||||
|
out.history_restoreDone = "Document restabilit";
|
||||||
|
out.history_version = "Versiune:";
|
||||||
|
out.poll_title = "Zero Knowledge Selector Dată";
|
||||||
|
out.poll_subtitle = "Zero Knowledge, <em>realtime</em> programare";
|
||||||
|
out.poll_p_save = "Setările tale sunt actualizate instant, așa că tu nu trebuie să salvezi.";
|
||||||
|
out.poll_p_encryption = "Tot conținutul tău este criptat ca doar persoanele cărora tu le dai link-ul să aibă acces. Nici serverul nu poate să vadă ce modifici.";
|
||||||
|
out.wizardLog = "Click pe butonul din dreapta sus pentru a te ntoarce la sondajul tău";
|
||||||
|
out.wizardTitle = "Folosește wizard-ul pentru a crea sondajul tău";
|
||||||
|
out.wizardConfirm = "Ești pregătit să adaugi aceste opțiuni la sondajul tău?";
|
||||||
|
out.poll_publish_button = "Publică";
|
||||||
|
out.poll_admin_button = "Admin";
|
||||||
|
out.poll_create_user = "Adaugă un nou utilizator";
|
||||||
|
out.poll_create_option = "Adaugă o nouă opțiune";
|
||||||
|
out.poll_commit = "Comite";
|
||||||
|
out.poll_closeWizardButton = "Închide wizard-ul";
|
||||||
|
out.poll_closeWizardButtonTitle = "Închide wizard-ul";
|
||||||
|
out.poll_wizardComputeButton = "Calculează Opțiunile";
|
||||||
|
out.poll_wizardClearButton = "Curăță Tabelul";
|
||||||
|
out.poll_wizardDescription = "Crează automat un număr de opțiuni întroducând orice număr de zile sau intervale orare";
|
||||||
|
|
||||||
|
out.poll_wizardAddDateButton = "+ Zi";
|
||||||
|
out.poll_wizardAddTimeButton = "+ Ore";
|
||||||
|
out.poll_optionPlaceholder = "Opțiune";
|
||||||
|
out.poll_userPlaceholder = "Numele tău";
|
||||||
|
out.poll_removeOption = "Ești sigur că vrei să îndepărtezi această opțiune?";
|
||||||
|
out.poll_removeUser = "Ești sigur că vrei să îndepărtezi aceast utilizator?";
|
||||||
|
out.poll_titleHint = "Titlu";
|
||||||
|
out.poll_descriptionHint = "Descrie sondajul, și apoi folosește butonul 'publică' când ai terminat. Orice utilizator care are link-ul poate modifica descrierea, dar descurajăm această practică.";
|
||||||
|
out.canvas_clear = "Curăță";
|
||||||
|
out.canvas_delete = "Curăță selecția";
|
||||||
|
out.canvas_disable = "Dezactivează modul desen";
|
||||||
|
out.canvas_enable = "Activează modul desen";
|
||||||
|
out.canvas_width = "Lățime";
|
||||||
|
out.canvas_opacity = "Opacitate";
|
||||||
|
out.fm_rootName = "Documente";
|
||||||
|
out.fm_trashName = "Gunoi";
|
||||||
|
out.fm_unsortedName = "Fișiere nesortate";
|
||||||
|
out.fm_filesDataName = "Toate fișierele";
|
||||||
|
out.fm_templateName = "Șabloane";
|
||||||
|
out.fm_searchName = "Caută";
|
||||||
|
out.fm_searchPlaceholder = "Caută...";
|
||||||
|
out.fm_newButton = "Nou";
|
||||||
|
out.fm_newButtonTitle = "Crează un nou pad sau folder";
|
||||||
|
out.fm_newFolder = "Folder nou";
|
||||||
|
out.fm_newFile = "Pad nou";
|
||||||
|
out.fm_folder = "Folder";
|
||||||
|
out.fm_folderName = "Numele folderului";
|
||||||
|
out.fm_numberOfFolders = "# de foldere";
|
||||||
|
out.fm_numberOfFiles = "# of files";
|
||||||
|
out.fm_fileName = "Nume filă";
|
||||||
|
out.fm_title = "Titlu";
|
||||||
|
out.fm_type = "Tip";
|
||||||
|
out.fm_lastAccess = "Ultima accesare";
|
||||||
|
out.fm_creation = "Creare";
|
||||||
|
out.fm_forbidden = "Acțiune interzisă";
|
||||||
|
out.fm_originalPath = "Ruta inițială";
|
||||||
|
out.fm_openParent = "Arată în folder";
|
||||||
|
out.fm_noname = "Document nedenumit";
|
||||||
|
out.fm_emptyTrashDialog = "Ești sigur că vrei să golești coșul de gunoi?";
|
||||||
|
out.fm_removeSeveralPermanentlyDialog = "Ești sigur că vrei să ștergi pentru totdeauna aceste {0} elemente din coșul de gunoi?";
|
||||||
|
out.fm_removePermanentlyDialog = "Ești sigur că vrei să ștergi acest element pentru totdeauna?";
|
||||||
|
out.fm_removeSeveralDialog = "Ești sigur că vrei să muți aceste {0} elemente la coșul de gunoi?";
|
||||||
|
out.fm_removeDialog = "Ești sigur că vrei să muți {0} la gunoi?";
|
||||||
|
out.fm_restoreDialog = "Ești sigur că vrei să restabilești {0} în locația trecută?";
|
||||||
|
out.fm_unknownFolderError = "Ultima locație vizitată sau cea selectată nu mai există. Deschidem fișierul părinte...";
|
||||||
|
out.fm_contextMenuError = "Nu putem deschide meniul de context pentru acest element. Dacă problema persistă, reîncarcă pagina.";
|
||||||
|
out.fm_selectError = "Nu putem selecta elementul vizat. Dacă problema persistă, reîncarcă pagina.";
|
||||||
|
out.fm_categoryError = "Nu putem deschide categoria selectată, afișează sursa.";
|
||||||
|
out.fm_info_root = "Crează câte foldere tip cuib ai nevoie pentru a-ți sorta fișierele.";
|
||||||
|
out.fm_info_unsorted = "Conține toate fișierele pe care le-ai vizitat și nu sunt sortate în \"Documente\" sau mutate în \"Gunoi\".";
|
||||||
|
out.fm_info_template = "Conține toate pad-urile stocate ca șabloane și pe care le poți refolosi atunci când creezi un nou pad.";
|
||||||
|
out.fm_info_trash = "Fișierele șterse din gunoi vor fi șterse și din \"Toate fișierele\", făcând imposibilă recuperarea fișierelor din managerul de fișiere.";
|
||||||
|
out.fm_info_allFiles = "Conține toate fișierele din \"Documente\", \"Nesortate\" și \"Gunoi\". Poți să muți sau să ștergi fișierele aici.";
|
||||||
|
out.fm_info_login = "Loghează-te";
|
||||||
|
out.fm_info_register = "Înscrie-te";
|
||||||
|
out.fm_info_anonymous = "Nu ești logat cu un cont valid așa că aceste pad-uri vor fi șterse (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">află de ce</a>). <a href=\"/register/\">Înscrie-te</a> sau <a href=\"/login/\">Loghează-te</a> pentru a le salva.";
|
||||||
|
out.fm_alert_backupUrl = "Link copie de rezervă pentru acest drive.<br> Este <strong>foarte recomandat</strong> să o păstrezi pentru tine.<br>Poți să o folosești pentru a recupera toate fișierele în cazul în care memoria browserului tău este șterge..<br>Oricine are linkul poate să editeze sau să îndepărteze toate fișierele din managerul tău de documente.<br>";
|
||||||
|
out.fm_alert_anonymous = "Salut, momentan folosești CryptPad în mod anonim. Este ok, doar că fișierele tale vor fi șterse după o perioadă de inactivitate. Am dezactivat caracteristicile avansate ale drive-ului pentru utilizatorii anonimi pentru a face clar faptul că stocare documentelor acolo nu este o metodă sigură. Poți să <a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">citești mai multe</a> despre motivarea noastră și despre ce de trebuie să te <a href=\"/register/\">Înregistrezi</a> si sa te <a href=\"/login/\">Loghezi</a>.";
|
||||||
|
out.fm_backup_title = "Link de backup";
|
||||||
|
out.fm_nameFile = "Cum ai vrea să numești fișierul?";
|
||||||
|
out.fc_newfolder = "Folder nou";
|
||||||
|
out.fc_rename = "Redenumește";
|
||||||
|
out.fc_open = "Deschide";
|
||||||
|
out.fc_open_ro = "Deschide (modul citire)";
|
||||||
|
out.fc_delete = "Șterge";
|
||||||
|
out.fc_restore = "Restaurează";
|
||||||
|
out.fc_remove = "Șterge permanent";
|
||||||
|
out.fc_empty = "Curăță coșul";
|
||||||
|
out.fc_prop = "Proprietăți";
|
||||||
|
out.fc_sizeInKilobytes = "Dimensiune n Kilobytes";
|
||||||
|
out.fo_moveUnsortedError = "Nu poți să muți un folder la lista de pad-uri nesortate";
|
||||||
|
out.fo_existingNameError = "Numele ales este deja folosit în acest director. Te rugăm să alegi altul.";
|
||||||
|
out.fo_moveFolderToChildError = "Nu poți să muți un folder într-unul dintre descendenții săi";
|
||||||
|
out.fo_unableToRestore = "Nu am reușit să restaurăm fișierul în locația de origine. Poți să ncerci să îl muți într-o nouă locație.";
|
||||||
|
out.fo_unavailableName = "Un fișier sau un folder cu același nume există deja în locația nouă. Redenumește elementul și încearcă din nou.";
|
||||||
|
out.login_login = "Loghează-te";
|
||||||
|
out.login_makeAPad = "Crează un pad în modul anonim";
|
||||||
|
out.login_nologin = "Răsfoiește pad-urile locale";
|
||||||
|
out.login_register = "Înscrie-te";
|
||||||
|
out.logoutButton = "Deloghează-te";
|
||||||
|
out.settingsButton = "Setări";
|
||||||
|
out.login_username = "Nume utilizator";
|
||||||
|
out.login_password = "Parolă";
|
||||||
|
out.login_confirm = "Confirmă parola";
|
||||||
|
out.login_remember = "Ține-mă minte";
|
||||||
|
out.login_hashing = "Încriptăm parola, o să mai dureze.";
|
||||||
|
out.login_hello = "Salut {0},";
|
||||||
|
out.login_helloNoName = "Salut,";
|
||||||
|
out.login_accessDrive = "Acesează-ți drive-ul";
|
||||||
|
out.login_orNoLogin = "sau";
|
||||||
|
out.login_noSuchUser = "Nume de utilizator sau parolă invalide. Încearcă din nou sau înscrie-te.";
|
||||||
|
out.login_invalUser = "Nume utilizator cerut";
|
||||||
|
out.login_invalPass = "Parolă cerută";
|
||||||
|
out.login_unhandledError = "O eroare neașteptată a avut loc emoticon_unhappy";
|
||||||
|
out.register_importRecent = "Importă istoricul pad-ului (Recomandat)";
|
||||||
|
out.register_acceptTerms = "Accept <a href='/terms.html'>termenii serviciului</a>";
|
||||||
|
out.register_passwordsDontMatch = "Parolele nu se potrivesc!";
|
||||||
|
out.register_mustAcceptTerms = "Trebuie să accepți termenii serviciului";
|
||||||
|
out.register_mustRememberPass = "Nu putem să îți resetăm parola dacă o uiți. Este foarte important să o ții minte! Bifează căsuța pentru a confirma.";
|
||||||
|
out.register_header = "Bine ai venit în CryptPad";
|
||||||
|
out.register_explanation = "<p>Hai să stabilim câteva lucruri, mai întâi</p><ul><li>Parola ta este cheia secretă care criptează toate pad-urile tale. Dacă pierzi/uiți parola nu există nici-o metodă prin care îți putem recupera datele.</li><li>Poți importa pad-uri care au fost vizionate recent în browser pentru a le avea în cont.</li><li>Dacă folosești un computer împărțit, trebuie să te deloghezi, închiderea taburilor nu este de ajuns.</li></ul>";
|
||||||
|
out.register_writtenPassword = "Mi-am notat numele de utilizator și parola, înaintează.";
|
||||||
|
out.register_cancel = "Întoarce-te";
|
||||||
|
out.register_warning = "Zero Knowledge înseamnă că noi nu îți putem recupera datele dacă îți pierzi parola.";
|
||||||
|
out.register_alreadyRegistered = "Acest user există deja, vrei să te loghezi?";
|
||||||
|
out.settings_title = "Setări";
|
||||||
|
out.settings_save = "Salvează";
|
||||||
|
out.settings_backupTitle = "Fă o copie de rezervă sau restaurează toate datele";
|
||||||
|
out.settings_backup = "Copie de rezervă";
|
||||||
|
out.settings_restore = "Restaurează";
|
||||||
|
out.settings_resetTitle = "Curăță-ți drive-ul";
|
||||||
|
out.settings_reset = "Îndepărtează toate fișierele și folderele din CryptPad-ul tău.";
|
||||||
|
out.settings_resetPrompt = "Această acțiune va indepărta toate pad-urile din drive-ul tău.<br>Ești sigur că vrei să continui?<br>Tastează “<em>Iubesc CryptPad</em>” pentru a confirma.";
|
||||||
|
out.settings_resetDone = "Drive-ul tău este acum gol!";
|
||||||
|
out.settings_resetError = "Text de verificare incorect. CryptPad-ul tău nu a fost schimbat.";
|
||||||
|
out.settings_resetTips = "Sfaturi în CryptDrive";
|
||||||
|
out.settings_resetTipsButton = "Resetează sfaturile disponibile în CryptDrive";
|
||||||
|
out.settings_resetTipsDone = "Toate sfaturile sunt vizibile din nou.";
|
||||||
|
out.settings_importTitle = "Importă pad-urile recente ale acestui browser n CryptDrive-ul meu";
|
||||||
|
out.settings_import = "Importă";
|
||||||
|
out.settings_importConfirm = "Ești sigur că vrei să imporți pad-urile recente ale acestui browser în contul tău de CryptDrive?";
|
||||||
|
out.settings_importDone = "Import complet";
|
||||||
|
out.settings_userFeedbackHint1 = "CryptPad oferă niște feedback foarte simplu serverului, pentru a ne informa cum putem să îți îmbunătățim experiența voastră.";
|
||||||
|
out.settings_userFeedbackHint2 = "Conținutul pad-ului tău nu va fi împărțit cu serverele.";
|
||||||
|
out.settings_userFeedback = "Activează feedback";
|
||||||
|
out.settings_anonymous = "Nu ești logat. Setările sunt specifice browser-ului.";
|
||||||
|
out.settings_publicSigningKey = "Cheia de semnătură publică";
|
||||||
|
out.settings_usage = "Uzaj";
|
||||||
|
out.settings_usageTitle = "Vezi dimensiunea totală a pad-urilor fixate în MB";
|
||||||
|
out.settings_pinningNotAvailable = "Pad-urile fixate sunt disponibile doar utilizatorilor înregistrați.";
|
||||||
|
out.settings_pinningError = "Ceva nu a funcționat";
|
||||||
|
out.settings_usageAmount = "Pad-urile tale fixate ocupă {0}MB";
|
||||||
|
out.settings_logoutEverywhereTitle = "Deloghează-te peste tot";
|
||||||
|
out.settings_logoutEverywhere = "Deloghează-te din toate sesiunile web";
|
||||||
|
out.settings_logoutEverywhereConfirm = "Ești sigur? Va trebui să te loghezi, din nou, pe toate device-urile tale.";
|
||||||
|
out.upload_serverError = "Eroare de server: fișierele tale nu pot fi încărcate la momentul acesta.";
|
||||||
|
out.upload_uploadPending = "Ai deja o încărcare în desfășurare. Anulezi și încarci noul fișier?";
|
||||||
|
out.upload_success = "Fișierul tău ({0}) a fost ncărcat și adăugat la drive-ul tău cu succes.";
|
||||||
|
out.main_p2 = "Acest proiect folosește <a href=\"http://ckeditor.com/\">CKEditor</a> Visual Editor, <a href=\"https://codemirror.net/\">CodeMirror</a>, și <a href=\"https://github.com/xwiki-contrib/chainpad\">ChainPad</a> un motor în timp real.";
|
||||||
|
out.main_howitworks_p1 = "CryptPad folosește o variantă a algoritmului de <a href=\"https://en.wikipedia.org/wiki/Operational_transformation\">Operational transformation</a> care este capabil să găsescă consens distribuit folosind <a href=\"https://bitcoin.org/bitcoin.pdf\">Nakamoto Blockchain</a>, o construcție popularizată de <a href=\"https://en.wikipedia.org/wiki/Bitcoin\">Bitcoin</a>. Astfel algoritmul poate evita nevoia ca serverul central să rezove conflicte, iar serverul nu este interesat de conținutul care este editat în pad.";
|
||||||
|
out.main_about_p2 = "Dacă ai orice fel de întrebare sau comentariu, poți să ne <a href=\"https://twitter.com/cryptpad\">dai un tweet</a>, semnalezi o problemă <a href=\"https://github.com/xwiki-labs/cryptpad/issues/\" title=\"index de probleme\">on github</a>, spui salut pe IRC (<a href=\"http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7\" title=\"freenode webchat\">irc.freenode.net</a>), sau <a href=\"research@xwiki.com\">trimiți un email</a>.";
|
||||||
|
out.main_info = "<h1>Colaborează n siguranță</h1><br> Dezvoltă-ți ideile împreună cu documente partajate în timp ce tehnologia <strong>Zero Knowledge</strong> îți păstrează securitatea; chiar și de noi.";
|
||||||
|
out.main_howitworks = "Cum funcționează";
|
||||||
|
out.main_zeroKnowledge = "Zero Knowledge";
|
||||||
|
out.main_zeroKnowledge_p = "Nu trebuie să ne crezi că <em>nu ne uităm</em> la pad-urile tale, cu tehnologia revoluționară Zero Knowledge a CryptPad <em>nu putem</em>. Învață mai multe despre cum îți protejăm <a href=\"/privacy.html\" title='Intimitatea'>Intimitate și Securitate</a>.";
|
||||||
|
out.main_writeItDown = "Notează";
|
||||||
|
out.main_writeItDown_p = "Cele mai importante proiecte vin din idei mici. Notează-ți momentele de inspirație și ideile neașteptate pentru că nu știi niciodată care ar putea fi noua mare descoperire.";
|
||||||
|
out.main_share = "Partajează link-ul, partajează pad-ul";
|
||||||
|
out.main_share_p = "Dezvoltă-ți ideile împreună: organizează întâlniri eficiente, colaborează pe liste TODO și fă prezentări scurte cu toți prietenii tăi și device-urile tale.";
|
||||||
|
out.main_organize = "Organizează-te";
|
||||||
|
out.main_organize_p = "Cu CryptPad Drive, poți să stai cu ochii pe ce este important. Folderele îți permit să ții evidența proiectelor tale și să ai o viziune globală asupra evoluției lucrurilor.";
|
||||||
|
out.tryIt = "Testează!";
|
||||||
|
out.main_richText = "Rich Text editor";
|
||||||
|
out.main_richText_p = "Editează texte complexe în mod colaborativ cu Zero Knowledge în timp real. <a href=\"http://ckeditor.com\" target=\"_blank\">CkEditor</a> application.";
|
||||||
|
out.main_code = "Editor cod";
|
||||||
|
out.main_code_p = "Editează cod din softul tău, în mod colaborativ, cu Zero Knowledge în timp real.<a href=\"https://www.codemirror.net\" target=\"_blank\">CodeMirror</a> application.";
|
||||||
|
out.main_slide = "Editor slide-uri";
|
||||||
|
out.main_slide_p = "Crează-ți prezentări folosind sintaxa Markdown, și afișează-le în browser-ul tău.";
|
||||||
|
out.main_poll = "Sondaj";
|
||||||
|
out.main_poll_p = "Plănuiește întâlniri sau evenimente, sau votează pentru cea mai bună soluție pentru problema ta.";
|
||||||
|
out.main_drive = "CryptDrive";
|
||||||
|
out.footer_applications = "Aplicații";
|
||||||
|
out.footer_contact = "Contact";
|
||||||
|
out.footer_aboutUs = "Despre noi";
|
||||||
|
out.about = "Despre";
|
||||||
|
out.privacy = "Privacy";
|
||||||
|
out.contact = "Contact";
|
||||||
|
out.terms = "ToS";
|
||||||
|
out.blog = "Blog";
|
||||||
|
out.policy_title = "Politica de confidențialitate CryptPad";
|
||||||
|
out.policy_whatweknow = "Ce știm despre tine";
|
||||||
|
out.policy_whatweknow_p1 = "Ca o aplicație care este găzduită online, CryptPad are acces la metadatele expuse de protocolul HTTP. Asta include adresa IP-ului tău, și alte titluri HTTP care pot fi folosite ca să identifice un browser. Poți să vezi ce informații împărtășește browser-ul tău vizitând <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.whatismybrowser.com/detect/what-http-headers-is-my-browser-sending\" title=\"what http headers is my browser sending\">WhatIsMyBrowser.com</a>.";
|
||||||
|
out.policy_whatweknow_p2 = "Folosim <a href=\"https://www.elastic.co/products/kibana\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"platforma de analiză open source\">Kibana</a>, o platformă open source, pentru a afla mai multe despre utilizatorii noștri. Kibana ne spune despre cum ai găsit CryptPad, căutare directă, printr-un motor de căutare, sau prin recomandare de la un alt serviciu online ca Reddit sau Twitter.";
|
||||||
|
out.policy_howweuse = "Cum folosim ce aflăm";
|
||||||
|
out.policy_howweuse_p1 = "Folosim aceste informații pentru a lua decizii mai bune în promovarea CryptPad, prin evaluarea eforturilor trecute care au fost de succes. Informațiile despre locația ta ne ajută să aflăm dacă ar trebui să oferim suport pentru alte limbi, pe lângă engleză.";
|
||||||
|
out.policy_howweuse_p2 = "Informațiile despre browser-ul tău (dacă este bazat pe un sistem de operare desktop sau mobil) ne ajută să luăm decizii când prioritizăm viitoarele îmbunătățiri. Echipa noastră de dezvoltare este mică, și încercăm să facem alegeri care să îmbunătățească experiența câtor mai mulți utilizatori.";
|
||||||
|
|
||||||
|
out.policy_whatwetell = "Ce le spunem altora despre tine";
|
||||||
|
out.policy_whatwetell_p1 = "Nu furnizăm informațiile obținute terților, decât dacă ne este cerut în mod legal.";
|
||||||
|
out.policy_links = "Link-uri către alte site-uri";
|
||||||
|
out.policy_links_p1 = "Acest site conține link-uri către alte site-uri, incluzându-le pe cele produse de alte organizații. Nu suntem responsabili pentru practicile de intimitate sau pentru conținutul site-urilor externe. Ca regulă generală, link-urile către site-uri externe sunt deschise ntr-o fereastră noup, pentru a face clar faptul că părăsiți CryptPad.fr.";
|
||||||
|
out.policy_ads = "Reclame";
|
||||||
|
out.policy_ads_p1 = "Nu afișăm nici o formă de publicitate online, dar s-ar putea să atașăm link-uri către instituțiile care ne finanțează cerecetarea.";
|
||||||
|
out.policy_choices = "Ce alegeri ai";
|
||||||
|
out.policy_choices_open = "Codul nostru este open source, așa că tu ai mereu posibilitatea de a-ți găzdui propria instanță de CryptPad.";
|
||||||
|
out.policy_choices_vpn = "Dacă vrei să folosești instanța găzduită de noi, dar nu vrei să îți expui IP-ul, poți să îl protejezi folosind <a href=\"https://www.torproject.org/projects/torbrowser.html.en\" title=\"downloads from the Tor project\" target=\"_blank\" rel=\"noopener noreferrer\">Tor browser bundle</a>, sau <a href=\"https://riseup.net/en/vpn\" title=\"VPNs provided by Riseup\" target=\"_blank\" rel=\"noopener noreferrer\">VPN</a>.";
|
||||||
|
out.policy_choices_ads = "Dacă vrei doar să blochezi platforma noastră de analiză, poți folosi soluții de adblocking ca <a href=\"https://www.eff.org/privacybadger\" title=\"download privacy badger\" target=\"_blank\" rel=\"noopener noreferrer\">Privacy Badger</a>.";
|
||||||
|
out.tos_title = "CryptPad Termeni de Utilizare";
|
||||||
|
out.tos_legal = "Te rugăm să nu fii rău intenționat, abuziv, sau să faci orice ilegal.";
|
||||||
|
out.tos_availability = "Sperăm că o să găsești acest serviciu util, dar disponibilitatea sau performanța nu poate fi garantată. Te rugăm să îți exporți datele n mod regulat.";
|
||||||
|
out.tos_e2ee = "Conținutul CryptPad poate fi citit sau modificat de oricine care poate ghici sau obține fragmentul identificator al pad-ului. Recomandăm să folosești soluții de comunicare criptate end-to-end-encrypted (e2ee) pentru a partaja link-uri, evitând orice risc în cazul unei scurgeri de informații.";
|
||||||
|
out.tos_logs = "Metadatele oferite de browser-ul tău serverului ar putea fi înscrise în scopul de a menține serviciul.";
|
||||||
|
out.tos_3rdparties = "Nu oferim date personale terților, decât dacă ne sunt solicitate prin lege.";
|
||||||
|
out.bottom_france = "<a href=\"http://www.xwiki.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Realizat cu <img class=\"bottom-bar-heart\" src=\"/customize/heart.png\" alt=\"love\" /> n <img class=\"bottom-bar-fr\" src=\"/customize/fr.png\" alt=\"Franța\" /></a>";
|
||||||
|
out.bottom_support = "<a href=\"http://labs.xwiki.com/\" title=\"XWiki Labs\" target=\"_blank\" rel=\"noopener noreferrer\">Un proiect al <img src=\"/customize/logo-xwiki2.png\" alt=\"XWiki SAS\" class=\"bottom-bar-xwiki\"/> Labs Project </a> cu susținerea <a href=\"http://ng.open-paas.org/\" title=\"OpenPaaS::ng\" target=\"_blank\" rel=\"noopener noreferrer\"> <img src=\"/customize/openpaasng.png\" alt=\"OpenPaaS-ng\" class=\"bottom-bar-openpaas\" /></a>";
|
||||||
|
out.header_france = "<a href=\"http://www.xwiki.com/\" target=\"_blank\" rel=\"noopener noreferrer\">With <img class=\"bottom-bar-heart\" src=\"/customize/heart.png\" alt=\"love\" /> from <img class=\"bottom-bar-fr\" src=\"/customize/fr.png\" title=\"Franța\" alt=\"Franța\"/> by <img src=\"/customize/logo-xwiki.png\" alt=\"XWiki SAS\" class=\"bottom-bar-xwiki\"/></a>";
|
||||||
|
out.header_support = "<a href=\"http://ng.open-paas.org/\" title=\"OpenPaaS::ng\" target=\"_blank\" rel=\"noopener noreferrer\"> <img src=\"/customize/openpaasng.png\" alt=\"OpenPaaS-ng\" class=\"bottom-bar-openpaas\" /></a>";
|
||||||
|
out.header_logoTitle = "Mergi la pagina principală";
|
||||||
|
out.initialState = "<span style=\"font-size:16px;\"><p>Acesta este <strong>CryptPad</strong>, editorul colaborativ bazat pe tehnologia Zero Knowledge în timp real. Totul este salvat pe măsură ce scrii.<br>Partajează link-ul către acest pad pentru a edita cu prieteni sau folosește <span style=\"background-color:#5cb85c;color:#ffffff;\"> Share </span> butonul pentru a partaja <em>read-only link</em> permițând vizualizarea dar nu și editarea.</p><p><span style=\"color:#808080;\"><em>Îndrăznește, începe să scrii...</em></span></p></span><p> <br></p>";
|
||||||
|
out.codeInitialState = "/*\n Acesta este editorul colaborativ de cod bazat pe tehnologia Zero Knowledge CryptPad.\n Ce scrii aici este criptat, așa că doar oamenii care au link-ul pot să-l acceseze.\n Poți să alegi ce limbaj de programare pus n evidență și schema de culori UI n dreapta sus.\n*/";
|
||||||
|
out.slideInitialState = "# CryptSlide\n* Acesta este un editor colaborativ bazat pe tehnologia Zero Knowledge.\n* Ce scrii aici este criptat, așa că doar oamenii care au link-ul pot să-l acceseze.\n* Nici măcar serverele nu au acces la ce scrii tu.\n* Ce vezi aici, ce auzi aici, atunci când pleci, lași aici.\n\n-\n# Cum se folosește\n1. Scrie-ți conținutul slide-urilor folosind sintaxa markdown\n - Află mai multe despre sintaxa markdown [aici](http://www.markdowntutorial.com/)\n2. Separă-ți slide-urile cu -\n3. Click pe butonul \"Play\" pentru a vedea rezultatele - Slide-urile tale sunt actualizate în timp real.";
|
||||||
|
out.driveReadmeTitle = "Ce este CryptDrive?";
|
||||||
|
out.readme_welcome = "Bine ai venit n CryptPad !";
|
||||||
|
out.readme_p1 = "Bine ai venit în CryptPad, acesta este locul unde îți poți lua notițe, singur sau cu prietenii.";
|
||||||
|
out.readme_p2 = "Acest pad o să îți ofere un scurt ghid în cum poți să folosești CryptPad pentru a lua notițe, a le ține organizate și a colabora pe ele.";
|
||||||
|
out.readme_cat1 = "Descoperă-ți CryptDrive-ul";
|
||||||
|
out.readme_cat1_l1 = "Crează un pad: În CryptDrive-ul tău, dă click {0} apoi {1} și poți să creezi un pad.";
|
||||||
|
out.readme_cat1_l2 = "Deschide pad-urile din CryptDrive-ul tău: doublu-click pe iconița unui pad pentru a-l deschide.";
|
||||||
|
out.readme_cat1_l3 = "Organizează-ți pad-urile: Când ești logat, orice pad accesezi va fi afișat ca în secțiunea {0} a drive-ului tău.";
|
||||||
|
out.readme_cat1_l3_l1 = "Poți să folosești funcția click and drag pentru a muta fișierele în folderele secțiunii {0} a drive-ului tău și pentru a crea noi foldere.";
|
||||||
|
out.readme_cat1_l3_l2 = "Ține minte să încerci click-dreapta pe iconițe pentru că există și meniuri adiționale.";
|
||||||
|
out.readme_cat1_l4 = "Pune pad-urile vechi în gunoi. Poți să folosești funcția click and drag pe pad-uri în categoria {0} la fel ca și în cazul folderelor.";
|
||||||
|
out.readme_cat2 = "Crează pad-uri ca un profesionist";
|
||||||
|
out.edit = "editează";
|
||||||
|
out.view = "vezi";
|
||||||
|
out.readme_cat2_l1 = "Butonul {0} din pad-ul tău dă accesul colaboratorilor tăi să {1} sau să {2} pad-ul.";
|
||||||
|
out.readme_cat2_l2 = "Schimbă titlul pad-ului dând click pe creion";
|
||||||
|
out.readme_cat3 = "Descoperă aplicațiile CryptPad";
|
||||||
|
out.readme_cat3_l1 = "Cu editorul de cod CryptPad, poți colabora pe cod ca Javascript și markdown ca HTML și Markdown";
|
||||||
|
out.readme_cat3_l2 = "Cu editorul de slide-uri CryptPad, poți să faci prezentări scurte folosind Markdown";
|
||||||
|
out.readme_cat3_l3 = "Cu CryptPoll poți să organizezi votări rapide, mai ales pentru a programa ntâlniri care se potrivesc calendarelor tuturor";
|
||||||
|
|
||||||
|
out.tips = { };
|
||||||
|
out.tips.lag = "Iconița verde din dreapta-sus arată calitatea conexiunii internetului tău la serverele CryptPad.";
|
||||||
|
out.tips.shortcuts = "`ctrl+b`, `ctrl+i` and `ctrl+u` sunt scurtături pentru bold, italic și underline.";
|
||||||
|
out.tips.indentare = "În listele cu bulină sau cele numerotate, poți folosi tab sau shift+tab pentru a mări sau micșora indentarea.";
|
||||||
|
out.tips.titlu = "Poți seta titlul pad-urilor tale prin click pe centru sus.";
|
||||||
|
out.tips.stocare = "De fiecare dată când vizitezi un pad, dacă ești logat va fi salvat pe CryptDrive-ul tău.";
|
||||||
|
out.tips.marker = "Poți sublinia text într-un pad folosind itemul \"marker\" n meniul de stiluri.";
|
||||||
|
|
||||||
|
out.feedback_about = "Dacă citești asta, probabil că ești curios de ce CryptPad cere pagini web atunci când întreprinzi anumite acțiuni";
|
||||||
|
out.feedback_privacy = "Ne pasă de intimitatea ta, si în același timp vrem să păstrăm CryptPad ușor de folosit. Folosim acest fișier pentru a ne da seama care beneficii UI contează cel mai mult pentru utilizatori, cerându-l alături de un parametru specific atunci când acțiunea se desfășoară";
|
||||||
|
out.feedback_optout = "Dacă vrei să ieși, vizitează <a href='/settings/'>setările de pe pagina ta de user</a>, unde vei găsi o căsuță pentru a activa sau dezactiva feedback-ul de la user";
|
||||||
|
|
||||||
|
return out;
|
||||||
|
});
|
@ -0,0 +1,101 @@
|
|||||||
|
/* jshint esversion: 6 */
|
||||||
|
const Fs = require('fs');
|
||||||
|
const Semaphore = require('saferphore');
|
||||||
|
const nThen = require('nthen');
|
||||||
|
|
||||||
|
const hashesFromPinFile = (pinFile, fileName) => {
|
||||||
|
var pins = {};
|
||||||
|
pinFile.split('\n').filter((x)=>(x)).map((l) => JSON.parse(l)).forEach((l) => {
|
||||||
|
switch (l[0]) {
|
||||||
|
case 'RESET': {
|
||||||
|
pins = {};
|
||||||
|
//jshint -W086
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
case 'PIN': {
|
||||||
|
l[1].forEach((x) => { pins[x] = 1; });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'UNPIN': {
|
||||||
|
l[1].forEach((x) => { delete pins[x]; });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: throw new Error(JSON.stringify(l) + ' ' + fileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Object.keys(pins);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeForHashes = (hashes, dsFileSizes) => {
|
||||||
|
let sum = 0;
|
||||||
|
hashes.forEach((h) => {
|
||||||
|
const s = dsFileSizes[h];
|
||||||
|
if (typeof(s) !== 'number') {
|
||||||
|
//console.log('missing ' + h + ' ' + typeof(s));
|
||||||
|
} else {
|
||||||
|
sum += s;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sum;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sema = Semaphore.create(20);
|
||||||
|
|
||||||
|
let dirList;
|
||||||
|
const fileList = [];
|
||||||
|
const dsFileSizes = {};
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
nThen((waitFor) => {
|
||||||
|
Fs.readdir('./datastore', waitFor((err, list) => {
|
||||||
|
if (err) { throw err; }
|
||||||
|
dirList = list;
|
||||||
|
}));
|
||||||
|
}).nThen((waitFor) => {
|
||||||
|
dirList.forEach((f) => {
|
||||||
|
sema.take((returnAfter) => {
|
||||||
|
Fs.readdir('./datastore/' + f, waitFor(returnAfter((err, list2) => {
|
||||||
|
if (err) { throw err; }
|
||||||
|
list2.forEach((ff) => { fileList.push('./datastore/' + f + '/' + ff); });
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).nThen((waitFor) => {
|
||||||
|
fileList.forEach((f) => {
|
||||||
|
sema.take((returnAfter) => {
|
||||||
|
Fs.stat(f, waitFor(returnAfter((err, st) => {
|
||||||
|
if (err) { throw err; }
|
||||||
|
dsFileSizes[f.replace(/^.*\/([^\/]*)\.ndjson$/, (all, a) => (a))] = st.size;
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).nThen((waitFor) => {
|
||||||
|
Fs.readdir('./pins', waitFor((err, list) => {
|
||||||
|
if (err) { throw err; }
|
||||||
|
dirList = list;
|
||||||
|
}));
|
||||||
|
}).nThen((waitFor) => {
|
||||||
|
fileList.splice(0, fileList.length);
|
||||||
|
dirList.forEach((f) => {
|
||||||
|
sema.take((returnAfter) => {
|
||||||
|
Fs.readdir('./pins/' + f, waitFor(returnAfter((err, list2) => {
|
||||||
|
if (err) { throw err; }
|
||||||
|
list2.forEach((ff) => { fileList.push('./pins/' + f + '/' + ff); });
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).nThen((waitFor) => {
|
||||||
|
fileList.forEach((f) => {
|
||||||
|
sema.take((returnAfter) => {
|
||||||
|
Fs.readFile(f, waitFor(returnAfter((err, content) => {
|
||||||
|
if (err) { throw err; }
|
||||||
|
const hashes = hashesFromPinFile(content.toString('utf8'), f);
|
||||||
|
const size = sizeForHashes(hashes, dsFileSizes);
|
||||||
|
out.push([f, Math.floor(size / (1024 * 1024))]);
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).nThen(() => {
|
||||||
|
out.sort((a,b) => (a[1] - b[1]));
|
||||||
|
out.forEach((x) => { console.log(x[0] + ' ' + x[1] + ' MB'); });
|
||||||
|
});
|
@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style>
|
|
||||||
media {
|
|
||||||
border: 1px solid black;
|
|
||||||
height: 100px;
|
|
||||||
width: 100px;
|
|
||||||
display: block;
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<media> my media thing </media>
|
|
||||||
<br />
|
|
||||||
<a href="https://www.w3schools.com/html/html5_new_elements.asp">valid elements</a>
|
|
||||||
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
|||||||
define([
|
|
||||||
'/bower_components/jquery/dist/jquery.min.js',
|
|
||||||
], function () {
|
|
||||||
var $ = window.jQuery;
|
|
||||||
|
|
||||||
$('media').each(function () {
|
|
||||||
window.alert("media tag selection works!");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style>
|
|
||||||
html{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.wrap {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
@ -1,25 +0,0 @@
|
|||||||
define([
|
|
||||||
'/bower_components/hyperjson/hyperjson.js',
|
|
||||||
'/bower_components/jquery/dist/jquery.min.js',
|
|
||||||
], function (Hyperjson) {
|
|
||||||
var $ = window.jQuery;
|
|
||||||
var shjson = '["BODY",{"class":"cke_editable cke_editable_themed cke_contents_ltr cke_show_borders","spellcheck":"false"},[["P",{},["This is ",["STRONG",{},["CryptPad"]],", the zero knowledge realtime collaborative editor.",["BR",{},[]],"What you type here is encrypted so only people who have the link can access it.",["BR",{},[]],"Even the server cannot see what you type."]],["P",{},[["SMALL",{},[["I",{},["What you see here, what you hear here, when you leave here, let it stay here"]]]],["BR",{"type":"_moz"},[]]]]]]';
|
|
||||||
|
|
||||||
var hjson = JSON.parse(shjson);
|
|
||||||
|
|
||||||
var pretty = Hyperjson.toString(hjson);
|
|
||||||
|
|
||||||
// set the body html to the rendered hyperjson
|
|
||||||
$('body')[0].outerHTML = pretty;
|
|
||||||
|
|
||||||
$('body')
|
|
||||||
// append the stringified-hyperjson source for reference
|
|
||||||
.append('<hr>').append($('<pre>', {
|
|
||||||
'class': 'wrap',
|
|
||||||
}).text(shjson))
|
|
||||||
// append the pretty-printed html source for reference
|
|
||||||
.append('<hr>').append($('<pre>').text(pretty));
|
|
||||||
|
|
||||||
|
|
||||||
// TODO write some tests to confirm whether the pretty printer is correct
|
|
||||||
});
|
|
@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="cp">
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||||
|
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="html">
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,59 @@
|
|||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/common/cryptpad-common.js',
|
||||||
|
'/bower_components/tweetnacl/nacl-fast.min.js'
|
||||||
|
], function ($, Cryptpad) {
|
||||||
|
var Nacl = window.nacl;
|
||||||
|
|
||||||
|
var signMsg = function (msg, privKey) {
|
||||||
|
var signKey = Nacl.util.decodeBase64(privKey);
|
||||||
|
var buffer = Nacl.util.decodeUTF8(msg);
|
||||||
|
return Nacl.util.encodeBase64(Nacl.sign(buffer, signKey));
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Allow authing for any domain as long as the user clicks an "accept" button
|
||||||
|
// inside of the iframe.
|
||||||
|
var AUTHORIZED_DOMAINS = [
|
||||||
|
/\.cryptpad\.fr$/,
|
||||||
|
/^http(s)?:\/\/localhost\:/
|
||||||
|
];
|
||||||
|
|
||||||
|
// Safari is weird about localStorage in iframes but seems to let sessionStorage slide.
|
||||||
|
localStorage.User_hash = localStorage.User_hash || sessionStorage.User_hash;
|
||||||
|
|
||||||
|
Cryptpad.ready(function () {
|
||||||
|
console.log('IFRAME READY');
|
||||||
|
$(window).on("message", function (jqe) {
|
||||||
|
var evt = jqe.originalEvent;
|
||||||
|
var data = JSON.parse(evt.data);
|
||||||
|
var domain = evt.origin;
|
||||||
|
var srcWindow = evt.source;
|
||||||
|
var ret = { txid: data.txid };
|
||||||
|
if (data.cmd === 'PING') {
|
||||||
|
ret.res = 'PONG';
|
||||||
|
} else if (data.cmd === 'SIGN') {
|
||||||
|
if (!AUTHORIZED_DOMAINS.filter(function (x) { return x.test(domain); }).length) {
|
||||||
|
ret.error = "UNAUTH_DOMAIN";
|
||||||
|
} else if (!Cryptpad.isLoggedIn()) {
|
||||||
|
ret.error = "NOT_LOGGED_IN";
|
||||||
|
} else {
|
||||||
|
var proxy = Cryptpad.getStore().getProxy().proxy;
|
||||||
|
var sig = signMsg(data.data, proxy.edPrivate);
|
||||||
|
ret.res = {
|
||||||
|
uname: proxy.login_name,
|
||||||
|
edPublic: proxy.edPublic,
|
||||||
|
sig: sig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (data.cmd === 'UPDATE_LIMIT') {
|
||||||
|
return Cryptpad.updatePinLimit(function (e, limit, plan, note) {
|
||||||
|
ret.res = [limit, plan, note];
|
||||||
|
srcWindow.postMessage(JSON.stringify(ret), domain);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ret.error = "UNKNOWN_CMD";
|
||||||
|
}
|
||||||
|
srcWindow.postMessage(JSON.stringify(ret), domain);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,300 @@
|
|||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/common/modes.js',
|
||||||
|
'/common/themes.js',
|
||||||
|
'/bower_components/file-saver/FileSaver.min.js'
|
||||||
|
], function ($, Modes, Themes) {
|
||||||
|
var saveAs = window.saveAs;
|
||||||
|
var module = {};
|
||||||
|
|
||||||
|
module.create = function (CMeditor, ifrw, Cryptpad) {
|
||||||
|
var exp = {};
|
||||||
|
|
||||||
|
var Messages = Cryptpad.Messages;
|
||||||
|
|
||||||
|
var CodeMirror = exp.CodeMirror = CMeditor;
|
||||||
|
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js";
|
||||||
|
|
||||||
|
var $pad = $('#pad-iframe');
|
||||||
|
var $textarea = exp.$textarea = $pad.contents().find('#editor1');
|
||||||
|
|
||||||
|
var Title;
|
||||||
|
var onLocal = function () {};
|
||||||
|
var $rightside;
|
||||||
|
exp.init = function (local, title, toolbar) {
|
||||||
|
if (typeof local === "function") {
|
||||||
|
onLocal = local;
|
||||||
|
}
|
||||||
|
Title = title;
|
||||||
|
$rightside = toolbar.$rightside;
|
||||||
|
};
|
||||||
|
|
||||||
|
var editor = exp.editor = CMeditor.fromTextArea($textarea[0], {
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
matchBrackets : true,
|
||||||
|
showTrailingSpace : true,
|
||||||
|
styleActiveLine : true,
|
||||||
|
search: true,
|
||||||
|
highlightSelectionMatches: {showToken: /\w+/},
|
||||||
|
extraKeys: {"Shift-Ctrl-R": undefined},
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||||
|
mode: "javascript",
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
editor.setValue(Messages.codeInitialState);
|
||||||
|
|
||||||
|
var setMode = exp.setMode = function (mode, cb) {
|
||||||
|
exp.highlightMode = mode;
|
||||||
|
if (mode === 'text') {
|
||||||
|
editor.setOption('mode', 'text');
|
||||||
|
if (cb) { cb('text'); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CMeditor.autoLoadMode(editor, mode);
|
||||||
|
editor.setOption('mode', mode);
|
||||||
|
if (exp.$language) {
|
||||||
|
var name = exp.$language.find('a[data-value="' + mode + '"]').text() || 'Mode';
|
||||||
|
exp.$language.setValue(name);
|
||||||
|
}
|
||||||
|
if(cb) { cb(mode); }
|
||||||
|
};
|
||||||
|
|
||||||
|
var setTheme = exp.setTheme = (function () {
|
||||||
|
var path = '/common/theme/';
|
||||||
|
|
||||||
|
var $head = $(ifrw.document.head);
|
||||||
|
|
||||||
|
var themeLoaded = exp.themeLoaded = function (theme) {
|
||||||
|
return $head.find('link[href*="'+theme+'"]').length;
|
||||||
|
};
|
||||||
|
|
||||||
|
var loadTheme = exp.loadTheme = function (theme) {
|
||||||
|
$head.append($('<link />', {
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href: path + theme + '.css',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (theme, $select) {
|
||||||
|
if (!theme) {
|
||||||
|
editor.setOption('theme', 'default');
|
||||||
|
} else {
|
||||||
|
if (!themeLoaded(theme)) {
|
||||||
|
loadTheme(theme);
|
||||||
|
}
|
||||||
|
editor.setOption('theme', theme);
|
||||||
|
}
|
||||||
|
if ($select) {
|
||||||
|
$select.setValue(theme || 'Theme');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
|
||||||
|
exp.getHeadingText = function () {
|
||||||
|
var lines = editor.getValue().split(/\n/);
|
||||||
|
|
||||||
|
var text = '';
|
||||||
|
lines.some(function (line) {
|
||||||
|
// lisps?
|
||||||
|
var lispy = /^\s*(;|#\|)(.*?)$/;
|
||||||
|
if (lispy.test(line)) {
|
||||||
|
line.replace(lispy, function (a, one, two) {
|
||||||
|
text = two;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lines beginning with a hash are potentially valuable
|
||||||
|
// works for markdown, python, bash, etc.
|
||||||
|
var hash = /^#(.*?)$/;
|
||||||
|
if (hash.test(line)) {
|
||||||
|
line.replace(hash, function (a, one) {
|
||||||
|
text = one;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lines including a c-style comment are also valuable
|
||||||
|
var clike = /^\s*(\/\*|\/\/)(.*)?(\*\/)*$/;
|
||||||
|
if (clike.test(line)) {
|
||||||
|
line.replace(clike, function (a, one, two) {
|
||||||
|
if (!(two && two.replace)) { return; }
|
||||||
|
text = two.replace(/\*\/\s*$/, '').trim();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO make one more pass for multiline comments
|
||||||
|
});
|
||||||
|
|
||||||
|
return text.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.configureLanguage = function (cb, onModeChanged) {
|
||||||
|
var options = [];
|
||||||
|
Modes.list.forEach(function (l) {
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {
|
||||||
|
'data-value': l.mode,
|
||||||
|
'href': '#',
|
||||||
|
},
|
||||||
|
content: l.language // Pretty name of the language value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var dropdownConfig = {
|
||||||
|
text: 'Mode', // Button initial text
|
||||||
|
options: options, // Entries displayed in the menu
|
||||||
|
left: true, // Open to the left of the button
|
||||||
|
isSelect: true,
|
||||||
|
};
|
||||||
|
var $block = exp.$language = Cryptpad.createDropdown(dropdownConfig);
|
||||||
|
$block.find('a').click(function () {
|
||||||
|
setMode($(this).attr('data-value'), onModeChanged);
|
||||||
|
onLocal();
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($rightside) { $rightside.append($block); }
|
||||||
|
if (cb) { cb(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.configureTheme = function (cb) {
|
||||||
|
/* Remember the user's last choice of theme using localStorage */
|
||||||
|
var themeKey = 'CRYPTPAD_CODE_THEME';
|
||||||
|
var lastTheme = localStorage.getItem(themeKey) || 'default';
|
||||||
|
|
||||||
|
var options = [];
|
||||||
|
Themes.forEach(function (l) {
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {
|
||||||
|
'data-value': l.name,
|
||||||
|
'href': '#',
|
||||||
|
},
|
||||||
|
content: l.name // Pretty name of the language value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var dropdownConfig = {
|
||||||
|
text: 'Theme', // Button initial text
|
||||||
|
options: options, // Entries displayed in the menu
|
||||||
|
left: true, // Open to the left of the button
|
||||||
|
isSelect: true,
|
||||||
|
initialValue: lastTheme
|
||||||
|
};
|
||||||
|
var $block = exp.$theme = Cryptpad.createDropdown(dropdownConfig);
|
||||||
|
|
||||||
|
setTheme(lastTheme, $block);
|
||||||
|
|
||||||
|
$block.find('a').click(function () {
|
||||||
|
var theme = $(this).attr('data-value');
|
||||||
|
setTheme(theme, $block);
|
||||||
|
localStorage.setItem(themeKey, theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($rightside) { $rightside.append($block); }
|
||||||
|
if (cb) { cb(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.exportText = function () {
|
||||||
|
var text = editor.getValue();
|
||||||
|
|
||||||
|
var ext = Modes.extensionOf(exp.highlightMode);
|
||||||
|
|
||||||
|
var title = Cryptpad.fixFileName(Title ? Title.suggestTitle('cryptpad') : "?") + (ext || '.txt');
|
||||||
|
|
||||||
|
Cryptpad.prompt(Messages.exportPrompt, title, function (filename) {
|
||||||
|
if (filename === null) { return; }
|
||||||
|
var blob = new Blob([text], {
|
||||||
|
type: 'text/plain;charset=utf-8'
|
||||||
|
});
|
||||||
|
saveAs(blob, filename);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
exp.importText = function (content, file) {
|
||||||
|
var $bar = ifrw.$('#cme_toolbox');
|
||||||
|
var mode;
|
||||||
|
var mime = CodeMirror.findModeByMIME(file.type);
|
||||||
|
|
||||||
|
if (!mime) {
|
||||||
|
var ext = /.+\.([^.]+)$/.exec(file.name);
|
||||||
|
if (ext[1]) {
|
||||||
|
mode = CMeditor.findModeByExtension(ext[1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mode = mime && mime.mode || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode && Modes.list.some(function (o) { return o.mode === mode; })) {
|
||||||
|
setMode(mode);
|
||||||
|
$bar.find('#language-mode').val(mode);
|
||||||
|
} else {
|
||||||
|
console.log("Couldn't find a suitable highlighting mode: %s", mode);
|
||||||
|
setMode('text');
|
||||||
|
$bar.find('#language-mode').val('text');
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.setValue(content);
|
||||||
|
onLocal();
|
||||||
|
};
|
||||||
|
|
||||||
|
var cursorToPos = function(cursor, oldText) {
|
||||||
|
var cLine = cursor.line;
|
||||||
|
var cCh = cursor.ch;
|
||||||
|
var pos = 0;
|
||||||
|
var textLines = oldText.split("\n");
|
||||||
|
for (var line = 0; line <= cLine; line++) {
|
||||||
|
if(line < cLine) {
|
||||||
|
pos += textLines[line].length+1;
|
||||||
|
}
|
||||||
|
else if(line === cLine) {
|
||||||
|
pos += cCh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
var posToCursor = function(position, newText) {
|
||||||
|
var cursor = {
|
||||||
|
line: 0,
|
||||||
|
ch: 0
|
||||||
|
};
|
||||||
|
var textLines = newText.substr(0, position).split("\n");
|
||||||
|
cursor.line = textLines.length - 1;
|
||||||
|
cursor.ch = textLines[cursor.line].length;
|
||||||
|
return cursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.setValueAndCursor = function (oldDoc, remoteDoc, TextPatcher) {
|
||||||
|
var scroll = editor.getScrollInfo();
|
||||||
|
//get old cursor here
|
||||||
|
var oldCursor = {};
|
||||||
|
oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
|
||||||
|
oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);
|
||||||
|
|
||||||
|
editor.setValue(remoteDoc);
|
||||||
|
editor.save();
|
||||||
|
|
||||||
|
var op = TextPatcher.diff(oldDoc, remoteDoc);
|
||||||
|
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
|
||||||
|
return TextPatcher.transformCursor(oldCursor[attr], op);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(selects[0] === selects[1]) {
|
||||||
|
editor.setCursor(posToCursor(selects[0], remoteDoc));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.scrollTo(scroll.left, scroll.top);
|
||||||
|
};
|
||||||
|
|
||||||
|
return exp;
|
||||||
|
};
|
||||||
|
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,322 @@
|
|||||||
|
define([
|
||||||
|
'/common/common-util.js',
|
||||||
|
'/common/common-interface.js',
|
||||||
|
'/bower_components/chainpad-crypto/crypto.js',
|
||||||
|
'/bower_components/tweetnacl/nacl-fast.min.js'
|
||||||
|
], function (Util, UI, Crypto) {
|
||||||
|
var Nacl = window.nacl;
|
||||||
|
|
||||||
|
var Hash = {};
|
||||||
|
|
||||||
|
var uint8ArrayToHex = Util.uint8ArrayToHex;
|
||||||
|
var hexToBase64 = Util.hexToBase64;
|
||||||
|
var base64ToHex = Util.base64ToHex;
|
||||||
|
|
||||||
|
// This implementation must match that on the server
|
||||||
|
// it's used for a checksum
|
||||||
|
Hash.hashChannelList = function (list) {
|
||||||
|
return Nacl.util.encodeBase64(Nacl.hash(Nacl.util
|
||||||
|
.decodeUTF8(JSON.stringify(list))));
|
||||||
|
};
|
||||||
|
|
||||||
|
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) {
|
||||||
|
if (typeof keys === 'string') {
|
||||||
|
return chanKey + keys;
|
||||||
|
}
|
||||||
|
if (!keys.editKeyStr) { return; }
|
||||||
|
return '/1/edit/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.editKeyStr)+'/';
|
||||||
|
};
|
||||||
|
var getViewHashFromKeys = Hash.getViewHashFromKeys = function (chanKey, keys) {
|
||||||
|
if (typeof keys === 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return '/1/view/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.viewKeyStr)+'/';
|
||||||
|
};
|
||||||
|
var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) {
|
||||||
|
return '/1/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/';
|
||||||
|
};
|
||||||
|
Hash.getUserHrefFromKeys = function (username, pubkey) {
|
||||||
|
return window.location.origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-');
|
||||||
|
};
|
||||||
|
|
||||||
|
var fixDuplicateSlashes = function (s) {
|
||||||
|
return s.replace(/\/+/g, '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Version 0
|
||||||
|
/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy
|
||||||
|
Version 1
|
||||||
|
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
|
||||||
|
*/
|
||||||
|
|
||||||
|
var parseTypeHash = Hash.parseTypeHash = function (type, hash) {
|
||||||
|
if (!hash) { return; }
|
||||||
|
var parsed = {};
|
||||||
|
var hashArr = fixDuplicateSlashes(hash).split('/');
|
||||||
|
if (['media', 'file', 'user'].indexOf(type) === -1) {
|
||||||
|
parsed.type = 'pad';
|
||||||
|
if (hash.slice(0,1) !== '/' && hash.length >= 56) {
|
||||||
|
// Old hash
|
||||||
|
parsed.channel = hash.slice(0, 32);
|
||||||
|
parsed.key = hash.slice(32, 56);
|
||||||
|
parsed.version = 0;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
if (hashArr[1] && hashArr[1] === '1') {
|
||||||
|
parsed.version = 1;
|
||||||
|
parsed.mode = hashArr[2];
|
||||||
|
parsed.channel = hashArr[3];
|
||||||
|
parsed.key = hashArr[4].replace(/-/g, '/');
|
||||||
|
parsed.present = typeof(hashArr[5]) === "string" && hashArr[5] === 'present';
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
if (['media', 'file'].indexOf(type) !== -1) {
|
||||||
|
parsed.type = 'file';
|
||||||
|
if (hashArr[1] && hashArr[1] === '1') {
|
||||||
|
parsed.version = 1;
|
||||||
|
parsed.channel = hashArr[2].replace(/-/g, '/');
|
||||||
|
parsed.key = hashArr[3].replace(/-/g, '/');
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
if (['user'].indexOf(type) !== -1) {
|
||||||
|
parsed.type = 'user';
|
||||||
|
if (hashArr[1] && hashArr[1] === '1') {
|
||||||
|
parsed.version = 1;
|
||||||
|
parsed.user = hashArr[2];
|
||||||
|
parsed.pubkey = hashArr[3].replace(/-/g, '/');
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var parsePadUrl = Hash.parsePadUrl = function (href) {
|
||||||
|
var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i;
|
||||||
|
|
||||||
|
var ret = {};
|
||||||
|
|
||||||
|
if (!href) { return ret; }
|
||||||
|
if (href.slice(-1) !== '/') { href += '/'; }
|
||||||
|
|
||||||
|
var idx;
|
||||||
|
|
||||||
|
if (!/^https*:\/\//.test(href)) {
|
||||||
|
idx = href.indexOf('/#');
|
||||||
|
ret.type = href.slice(1, idx);
|
||||||
|
ret.hash = href.slice(idx + 2);
|
||||||
|
ret.hashData = parseTypeHash(ret.type, ret.hash);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
href.replace(patt, function (a, domain, type) {
|
||||||
|
ret.domain = domain;
|
||||||
|
ret.type = type;
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
idx = href.indexOf('/#');
|
||||||
|
ret.hash = href.slice(idx + 2);
|
||||||
|
ret.hashData = parseTypeHash(ret.type, ret.hash);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getRelativeHref = Hash.getRelativeHref = function (href) {
|
||||||
|
if (!href) { return; }
|
||||||
|
if (href.indexOf('#') === -1) { return; }
|
||||||
|
var parsed = parsePadUrl(href);
|
||||||
|
return '/' + parsed.type + '/#' + parsed.hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns all needed keys for a realtime channel
|
||||||
|
* - no argument: use the URL hash or create one if it doesn't exist
|
||||||
|
* - secretHash provided: use secretHash to find the keys
|
||||||
|
*/
|
||||||
|
Hash.getSecrets = function (type, secretHash) {
|
||||||
|
var secret = {};
|
||||||
|
var generate = function () {
|
||||||
|
secret.keys = Crypto.createEditCryptor();
|
||||||
|
secret.key = Crypto.createEditCryptor().editKeyStr;
|
||||||
|
};
|
||||||
|
if (!secretHash && !/#/.test(window.location.href)) {
|
||||||
|
generate();
|
||||||
|
return secret;
|
||||||
|
} else {
|
||||||
|
var parsed;
|
||||||
|
var hash;
|
||||||
|
if (secretHash) {
|
||||||
|
if (!type) { throw new Error("getSecrets with a hash requires a type parameter"); }
|
||||||
|
parsed = parseTypeHash(type, secretHash);
|
||||||
|
hash = secretHash;
|
||||||
|
} else {
|
||||||
|
var pHref = parsePadUrl(window.location.href);
|
||||||
|
parsed = pHref.hashData;
|
||||||
|
hash = pHref.hash;
|
||||||
|
}
|
||||||
|
//var parsed = parsePadUrl(window.location.href);
|
||||||
|
//var hash = secretHash || window.location.hash.slice(1);
|
||||||
|
if (hash.length === 0) {
|
||||||
|
generate();
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
// old hash system : #{hexChanKey}{cryptKey}
|
||||||
|
// new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey}
|
||||||
|
if (parsed.version === 0) {
|
||||||
|
// Old hash
|
||||||
|
secret.channel = parsed.channel;
|
||||||
|
secret.key = parsed.key;
|
||||||
|
}
|
||||||
|
else if (parsed.version === 1) {
|
||||||
|
// New hash
|
||||||
|
if (parsed.type === "pad") {
|
||||||
|
secret.channel = base64ToHex(parsed.channel);
|
||||||
|
if (parsed.mode === 'edit') {
|
||||||
|
secret.keys = Crypto.createEditCryptor(parsed.key);
|
||||||
|
secret.key = secret.keys.editKeyStr;
|
||||||
|
if (secret.channel.length !== 32 || secret.key.length !== 24) {
|
||||||
|
UI.alert("The channel key and/or the encryption key is invalid");
|
||||||
|
throw new Error("The channel key and/or the encryption key is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (parsed.mode === 'view') {
|
||||||
|
secret.keys = Crypto.createViewCryptor(parsed.key);
|
||||||
|
if (secret.channel.length !== 32) {
|
||||||
|
UI.alert("The channel key is invalid");
|
||||||
|
throw new Error("The channel key is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parsed.type === "file") {
|
||||||
|
// version 2 hashes are to be used for encrypted blobs
|
||||||
|
secret.channel = parsed.channel;
|
||||||
|
secret.keys = { fileKeyStr: parsed.key };
|
||||||
|
} else if (parsed.type === "user") {
|
||||||
|
// version 2 hashes are to be used for encrypted blobs
|
||||||
|
throw new Error("User hashes can't be opened (yet)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
Hash.getHashes = function (channel, secret) {
|
||||||
|
var hashes = {};
|
||||||
|
if (secret.keys.editKeyStr) {
|
||||||
|
hashes.editHash = getEditHashFromKeys(channel, secret.keys);
|
||||||
|
}
|
||||||
|
if (secret.keys.viewKeyStr) {
|
||||||
|
hashes.viewHash = getViewHashFromKeys(channel, secret.keys);
|
||||||
|
}
|
||||||
|
if (secret.keys.fileKeyStr) {
|
||||||
|
hashes.fileHash = getFileHashFromKeys(channel, secret.keys.fileKeyStr);
|
||||||
|
}
|
||||||
|
return hashes;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createChannelId = Hash.createChannelId = function () {
|
||||||
|
var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16));
|
||||||
|
if (id.length !== 32 || /[^a-f0-9]/.test(id)) {
|
||||||
|
throw new Error('channel ids must consist of 32 hex characters');
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
Hash.createRandomHash = function () {
|
||||||
|
// 16 byte channel Id
|
||||||
|
var channelId = Util.hexToBase64(createChannelId());
|
||||||
|
// 18 byte encryption key
|
||||||
|
var key = Crypto.b64RemoveSlashes(Crypto.rand64(18));
|
||||||
|
return '/1/edit/' + [channelId, key].join('/') + '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
// STORAGE
|
||||||
|
Hash.findWeaker = function (href, recents) {
|
||||||
|
var rHref = href || getRelativeHref(window.location.href);
|
||||||
|
var parsed = parsePadUrl(rHref);
|
||||||
|
if (!parsed.hash) { return false; }
|
||||||
|
var weaker;
|
||||||
|
recents.some(function (pad) {
|
||||||
|
var p = parsePadUrl(pad.href);
|
||||||
|
if (p.type !== parsed.type) { return; } // Not the same type
|
||||||
|
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
|
||||||
|
var pHash = p.hashData;
|
||||||
|
var parsedHash = parsed.hashData;
|
||||||
|
if (!parsedHash || !pHash) { return; }
|
||||||
|
|
||||||
|
// We don't have stronger/weaker versions of files or users
|
||||||
|
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
|
||||||
|
|
||||||
|
if (pHash.version !== parsedHash.version) { return; }
|
||||||
|
if (pHash.channel !== parsedHash.channel) { return; }
|
||||||
|
if (pHash.mode === 'view' && parsedHash.mode === 'edit') {
|
||||||
|
weaker = pad.href;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
return weaker;
|
||||||
|
};
|
||||||
|
var findStronger = Hash.findStronger = function (href, recents) {
|
||||||
|
var rHref = href || getRelativeHref(window.location.href);
|
||||||
|
var parsed = parsePadUrl(rHref);
|
||||||
|
if (!parsed.hash) { return false; }
|
||||||
|
var stronger;
|
||||||
|
recents.some(function (pad) {
|
||||||
|
var p = parsePadUrl(pad.href);
|
||||||
|
if (p.type !== parsed.type) { return; } // Not the same type
|
||||||
|
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
|
||||||
|
var pHash = p.hashData;
|
||||||
|
var parsedHash = parsed.hashData;
|
||||||
|
if (!parsedHash || !pHash) { return; }
|
||||||
|
|
||||||
|
// We don't have stronger/weaker versions of files or users
|
||||||
|
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
|
||||||
|
|
||||||
|
if (pHash.version !== parsedHash.version) { return; }
|
||||||
|
if (pHash.channel !== parsedHash.channel) { return; }
|
||||||
|
if (pHash.mode === 'edit' && parsedHash.mode === 'view') {
|
||||||
|
stronger = pad.href;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
return stronger;
|
||||||
|
};
|
||||||
|
Hash.isNotStrongestStored = function (href, recents) {
|
||||||
|
return findStronger(href, recents);
|
||||||
|
};
|
||||||
|
|
||||||
|
Hash.hrefToHexChannelId = function (href) {
|
||||||
|
var parsed = Hash.parsePadUrl(href);
|
||||||
|
if (!parsed || !parsed.hash) { return; }
|
||||||
|
|
||||||
|
parsed = parsed.hashData;
|
||||||
|
if (parsed.version === 0) {
|
||||||
|
return parsed.channel;
|
||||||
|
} else if (parsed.version !== 1 && parsed.version !== 2) {
|
||||||
|
console.error("parsed href had no version");
|
||||||
|
console.error(parsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel = parsed.channel;
|
||||||
|
if (!channel) { return; }
|
||||||
|
|
||||||
|
var hex = base64ToHex(channel);
|
||||||
|
return hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
Hash.getBlobPathFromHex = function (id) {
|
||||||
|
return '/blob/' + id.slice(0,2) + '/' + id;
|
||||||
|
};
|
||||||
|
|
||||||
|
Hash.serializeHash = function (hash) {
|
||||||
|
if (hash && hash.slice(-1) !== "/") { hash += "/"; }
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Hash;
|
||||||
|
});
|
@ -0,0 +1,249 @@
|
|||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||||
|
'/bower_components/chainpad-crypto/crypto.js',
|
||||||
|
'/bower_components/chainpad/chainpad.dist.js',
|
||||||
|
], function ($, JsonOT, Crypto) {
|
||||||
|
var ChainPad = window.ChainPad;
|
||||||
|
var History = {};
|
||||||
|
|
||||||
|
var getStates = function (rt) {
|
||||||
|
var states = [];
|
||||||
|
var b = rt.getAuthBlock();
|
||||||
|
if (b) { states.unshift(b); }
|
||||||
|
while (b.getParent()) {
|
||||||
|
b = b.getParent();
|
||||||
|
states.unshift(b);
|
||||||
|
}
|
||||||
|
return states;
|
||||||
|
};
|
||||||
|
|
||||||
|
var loadHistory = function (config, common, cb) {
|
||||||
|
var network = common.getNetwork();
|
||||||
|
var hkn = network.historyKeeper;
|
||||||
|
|
||||||
|
var wcId = common.hrefToHexChannelId(config.href || window.location.href);
|
||||||
|
|
||||||
|
var createRealtime = function () {
|
||||||
|
return ChainPad.create({
|
||||||
|
userName: 'history',
|
||||||
|
initialState: '',
|
||||||
|
transformFunction: JsonOT.validate,
|
||||||
|
logLevel: 0,
|
||||||
|
noPrune: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var realtime = createRealtime();
|
||||||
|
|
||||||
|
var parsed = config.href ? common.parsePadUrl(config.href) : {};
|
||||||
|
var secret = common.getSecrets(parsed.type, parsed.hash);
|
||||||
|
var crypto = Crypto.createEncryptor(secret.keys);
|
||||||
|
|
||||||
|
var to = window.setTimeout(function () {
|
||||||
|
cb('[GET_FULL_HISTORY_TIMEOUT]');
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
var parse = function (msg) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(msg);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var onMsg = function (msg) {
|
||||||
|
var parsed = parse(msg);
|
||||||
|
if (parsed[0] === 'FULL_HISTORY_END') {
|
||||||
|
console.log('END');
|
||||||
|
window.clearTimeout(to);
|
||||||
|
cb(null, realtime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsed[0] !== 'FULL_HISTORY') { return; }
|
||||||
|
msg = parsed[1][4];
|
||||||
|
if (msg) {
|
||||||
|
msg = msg.replace(/^cp\|/, '');
|
||||||
|
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey);
|
||||||
|
realtime.message(decryptedMsg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
network.on('message', function (msg) {
|
||||||
|
onMsg(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', wcId, secret.keys.validateKey]));
|
||||||
|
};
|
||||||
|
|
||||||
|
History.create = function (common, config) {
|
||||||
|
if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
|
||||||
|
if (History.loading) { return void console.error("History is already being loaded..."); }
|
||||||
|
History.loading = true;
|
||||||
|
var $toolbar = config.$toolbar;
|
||||||
|
|
||||||
|
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
|
||||||
|
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// config.setHistory(bool, bool)
|
||||||
|
// - bool1: history value
|
||||||
|
// - bool2: reset old content?
|
||||||
|
var render = function (val) {
|
||||||
|
if (typeof val === "undefined") { return; }
|
||||||
|
try {
|
||||||
|
config.applyVal(val);
|
||||||
|
} catch (e) {
|
||||||
|
// Probably a parse error
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var onClose = function () { config.setHistory(false, true); };
|
||||||
|
var onRevert = function () {
|
||||||
|
config.setHistory(false, false);
|
||||||
|
config.onLocal();
|
||||||
|
config.onRemote();
|
||||||
|
};
|
||||||
|
var onReady = function () {
|
||||||
|
config.setHistory(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
var Messages = common.Messages;
|
||||||
|
|
||||||
|
var realtime;
|
||||||
|
|
||||||
|
var states = [];
|
||||||
|
var c = states.length - 1;
|
||||||
|
|
||||||
|
var $hist = $toolbar.find('.cryptpad-toolbar-history');
|
||||||
|
var $left = $toolbar.find('.cryptpad-toolbar-leftside');
|
||||||
|
var $right = $toolbar.find('.cryptpad-toolbar-rightside');
|
||||||
|
var $cke = $toolbar.find('.cke_toolbox_main');
|
||||||
|
|
||||||
|
var onUpdate;
|
||||||
|
|
||||||
|
var update = function () {
|
||||||
|
if (!realtime) { return []; }
|
||||||
|
states = getStates(realtime);
|
||||||
|
if (typeof onUpdate === "function") { onUpdate(); }
|
||||||
|
return states;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the content of the selected version, and change the version number
|
||||||
|
var get = function (i) {
|
||||||
|
i = parseInt(i);
|
||||||
|
if (isNaN(i)) { return; }
|
||||||
|
if (i < 0) { i = 0; }
|
||||||
|
if (i > states.length - 1) { i = states.length - 1; }
|
||||||
|
var val = states[i].getContent().doc;
|
||||||
|
c = i;
|
||||||
|
if (typeof onUpdate === "function") { onUpdate(); }
|
||||||
|
$hist.find('.next, .previous').css('visibility', '');
|
||||||
|
if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); }
|
||||||
|
if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); }
|
||||||
|
return val || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
var getNext = function (step) {
|
||||||
|
return typeof step === "number" ? get(c + step) : get(c + 1);
|
||||||
|
};
|
||||||
|
var getPrevious = function (step) {
|
||||||
|
return typeof step === "number" ? get(c - step) : get(c - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the history toolbar
|
||||||
|
var display = function () {
|
||||||
|
$hist.html('').show();
|
||||||
|
$left.hide();
|
||||||
|
$right.hide();
|
||||||
|
$cke.hide();
|
||||||
|
var $prev =$('<button>', {
|
||||||
|
'class': 'previous fa fa-step-backward buttonPrimary',
|
||||||
|
title: Messages.history_prev
|
||||||
|
}).appendTo($hist);
|
||||||
|
var $nav = $('<div>', {'class': 'goto'}).appendTo($hist);
|
||||||
|
var $next = $('<button>', {
|
||||||
|
'class': 'next fa fa-step-forward buttonPrimary',
|
||||||
|
title: Messages.history_next
|
||||||
|
}).appendTo($hist);
|
||||||
|
|
||||||
|
$('<label>').text(Messages.history_version).appendTo($nav);
|
||||||
|
var $cur = $('<input>', {
|
||||||
|
'class' : 'gotoInput',
|
||||||
|
'type' : 'number',
|
||||||
|
'min' : '1',
|
||||||
|
'max' : states.length
|
||||||
|
}).val(c + 1).appendTo($nav).mousedown(function (e) {
|
||||||
|
// stopPropagation because the event would be cancelled by the dropdown menus
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
var $label2 = $('<label>').text(' / '+ states.length).appendTo($nav);
|
||||||
|
$('<br>').appendTo($nav);
|
||||||
|
var $close = $('<button>', {
|
||||||
|
'class':'closeHistory',
|
||||||
|
title: Messages.history_closeTitle
|
||||||
|
}).text(Messages.history_close).appendTo($nav);
|
||||||
|
var $rev = $('<button>', {
|
||||||
|
'class':'revertHistory buttonSuccess',
|
||||||
|
title: Messages.history_restoreTitle
|
||||||
|
}).text(Messages.history_restore).appendTo($nav);
|
||||||
|
|
||||||
|
onUpdate = function () {
|
||||||
|
$cur.attr('max', states.length);
|
||||||
|
$cur.val(c+1);
|
||||||
|
$label2.text(' / ' + states.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
var close = function () {
|
||||||
|
$hist.hide();
|
||||||
|
$left.show();
|
||||||
|
$right.show();
|
||||||
|
$cke.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buttons actions
|
||||||
|
$prev.click(function () { render(getPrevious()); });
|
||||||
|
$next.click(function () { render(getNext()); });
|
||||||
|
$cur.keydown(function (e) {
|
||||||
|
var p = function () { e.preventDefault(); };
|
||||||
|
if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter
|
||||||
|
if ([37, 40].indexOf(e.which) >= 0) { p(); return render(getPrevious()); } // Left
|
||||||
|
if ([38, 39].indexOf(e.which) >= 0) { p(); return render(getNext()); } // Right
|
||||||
|
if (e.which === 33) { p(); return render(getNext(10)); } // PageUp
|
||||||
|
if (e.which === 34) { p(); return render(getPrevious(10)); } // PageUp
|
||||||
|
if (e.which === 27) { p(); $close.click(); }
|
||||||
|
}).focus();
|
||||||
|
$cur.on('change', function () {
|
||||||
|
render( get($cur.val() - 1) );
|
||||||
|
});
|
||||||
|
$close.click(function () {
|
||||||
|
states = [];
|
||||||
|
close();
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
$rev.click(function () {
|
||||||
|
common.confirm(Messages.history_restorePrompt, function (yes) {
|
||||||
|
if (!yes) { return; }
|
||||||
|
close();
|
||||||
|
onRevert();
|
||||||
|
common.log(Messages.history_restoreDone);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display the latest content
|
||||||
|
render(get(c));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load all the history messages into a new chainpad object
|
||||||
|
loadHistory(config, common, function (err, newRt) {
|
||||||
|
History.loading = false;
|
||||||
|
if (err) { throw new Error(err); }
|
||||||
|
realtime = newRt;
|
||||||
|
update();
|
||||||
|
c = states.length - 1;
|
||||||
|
display();
|
||||||
|
onReady();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return History;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,244 @@
|
|||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/customize/messages.js',
|
||||||
|
'/common/common-util.js',
|
||||||
|
'/customize/application_config.js',
|
||||||
|
'/bower_components/alertifyjs/dist/js/alertify.js',
|
||||||
|
'/common/notify.js',
|
||||||
|
'/common/visible.js'
|
||||||
|
], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible) {
|
||||||
|
|
||||||
|
var UI = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Alertifyjs
|
||||||
|
*/
|
||||||
|
UI.Alertify = Alertify;
|
||||||
|
|
||||||
|
// set notification timeout
|
||||||
|
Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000;
|
||||||
|
|
||||||
|
var findCancelButton = UI.findCancelButton = function () {
|
||||||
|
return $('button.cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
var findOKButton = UI.findOKButton = function () {
|
||||||
|
return $('button.ok');
|
||||||
|
};
|
||||||
|
|
||||||
|
var listenForKeys = UI.listenForKeys = function (yes, no) {
|
||||||
|
var handler = function (e) {
|
||||||
|
switch (e.which) {
|
||||||
|
case 27: // cancel
|
||||||
|
if (typeof(no) === 'function') { no(e); }
|
||||||
|
no();
|
||||||
|
break;
|
||||||
|
case 13: // enter
|
||||||
|
if (typeof(yes) === 'function') { yes(e); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$(window).keyup(handler);
|
||||||
|
return handler;
|
||||||
|
};
|
||||||
|
|
||||||
|
var stopListening = UI.stopListening = function (handler) {
|
||||||
|
$(window).off('keyup', handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
UI.alert = function (msg, cb, force) {
|
||||||
|
cb = cb || function () {};
|
||||||
|
if (force !== true) { msg = Util.fixHTML(msg); }
|
||||||
|
var close = function () {
|
||||||
|
findOKButton().click();
|
||||||
|
};
|
||||||
|
var keyHandler = listenForKeys(close, close);
|
||||||
|
Alertify.alert(msg, function (ev) {
|
||||||
|
cb(ev);
|
||||||
|
stopListening(keyHandler);
|
||||||
|
});
|
||||||
|
window.setTimeout(function () {
|
||||||
|
findOKButton().focus();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
UI.prompt = function (msg, def, cb, opt, force) {
|
||||||
|
opt = opt || {};
|
||||||
|
cb = cb || function () {};
|
||||||
|
if (force !== true) { msg = Util.fixHTML(msg); }
|
||||||
|
|
||||||
|
var keyHandler = listenForKeys(function () { // yes
|
||||||
|
findOKButton().click();
|
||||||
|
}, function () { // no
|
||||||
|
findCancelButton().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
Alertify
|
||||||
|
.defaultValue(def || '')
|
||||||
|
.okBtn(opt.ok || Messages.okButton || 'OK')
|
||||||
|
.cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel')
|
||||||
|
.prompt(msg, function (val, ev) {
|
||||||
|
cb(val, ev);
|
||||||
|
stopListening(keyHandler);
|
||||||
|
}, function (ev) {
|
||||||
|
cb(null, ev);
|
||||||
|
stopListening(keyHandler);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
UI.confirm = function (msg, cb, opt, force, styleCB) {
|
||||||
|
opt = opt || {};
|
||||||
|
cb = cb || function () {};
|
||||||
|
if (force !== true) { msg = Util.fixHTML(msg); }
|
||||||
|
|
||||||
|
var keyHandler = listenForKeys(function () {
|
||||||
|
findOKButton().click();
|
||||||
|
}, function () {
|
||||||
|
findCancelButton().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
Alertify
|
||||||
|
.okBtn(opt.ok || Messages.okButton || 'OK')
|
||||||
|
.cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel')
|
||||||
|
.confirm(msg, function () {
|
||||||
|
cb(true);
|
||||||
|
stopListening(keyHandler);
|
||||||
|
}, function () {
|
||||||
|
cb(false);
|
||||||
|
stopListening(keyHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.setTimeout(function () {
|
||||||
|
var $ok = findOKButton();
|
||||||
|
var $cancel = findCancelButton();
|
||||||
|
if (opt.okClass) { $ok.addClass(opt.okClass); }
|
||||||
|
if (opt.cancelClass) { $cancel.addClass(opt.cancelClass); }
|
||||||
|
if (opt.reverseOrder) {
|
||||||
|
$ok.insertBefore($ok.prev());
|
||||||
|
}
|
||||||
|
if (typeof(styleCB) === 'function') {
|
||||||
|
styleCB($ok.closest('.dialog'));
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
UI.log = function (msg) {
|
||||||
|
Alertify.success(Util.fixHTML(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
UI.warn = function (msg) {
|
||||||
|
Alertify.error(Util.fixHTML(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* spinner
|
||||||
|
*/
|
||||||
|
UI.spinner = function (parent) {
|
||||||
|
var $target = $('<span>', {
|
||||||
|
'class': 'fa fa-spinner fa-pulse fa-4x fa-fw'
|
||||||
|
}).hide();
|
||||||
|
|
||||||
|
$(parent).append($target);
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function () {
|
||||||
|
$target.css('display', 'inline');
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
hide: function () {
|
||||||
|
$target.hide();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
get: function () {
|
||||||
|
return $target;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var LOADING = 'loading';
|
||||||
|
|
||||||
|
var getRandomTip = function () {
|
||||||
|
if (!Messages.tips || !Object.keys(Messages.tips).length) { return ''; }
|
||||||
|
var keys = Object.keys(Messages.tips);
|
||||||
|
var rdm = Math.floor(Math.random() * keys.length);
|
||||||
|
return Messages.tips[keys[rdm]];
|
||||||
|
};
|
||||||
|
UI.addLoadingScreen = function (loadingText, hideTips) {
|
||||||
|
var $loading, $container;
|
||||||
|
if ($('#' + LOADING).length) {
|
||||||
|
$loading = $('#' + LOADING).show();
|
||||||
|
if (loadingText) {
|
||||||
|
$('#' + LOADING).find('p').text(loadingText);
|
||||||
|
}
|
||||||
|
$container = $loading.find('.loadingContainer');
|
||||||
|
} else {
|
||||||
|
$loading = $('<div>', {id: LOADING});
|
||||||
|
$container = $('<div>', {'class': 'loadingContainer'});
|
||||||
|
$container.append('<img class="cryptofist" src="/customize/cryptofist_small.png" />');
|
||||||
|
var $spinner = $('<div>', {'class': 'spinnerContainer'});
|
||||||
|
UI.spinner($spinner).show();
|
||||||
|
var $text = $('<p>').text(loadingText || Messages.loading);
|
||||||
|
$container.append($spinner).append($text);
|
||||||
|
$loading.append($container);
|
||||||
|
$('body').append($loading);
|
||||||
|
}
|
||||||
|
if (Messages.tips && !hideTips) {
|
||||||
|
var $loadingTip = $('<div>', {'id': 'loadingTip'});
|
||||||
|
$('<span>', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip);
|
||||||
|
$loadingTip.css({
|
||||||
|
'top': $('body').height()/2 + $container.height()/2 + 20 + 'px'
|
||||||
|
});
|
||||||
|
$('body').append($loadingTip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UI.removeLoadingScreen = function (cb) {
|
||||||
|
$('#' + LOADING).fadeOut(750, cb);
|
||||||
|
$('#loadingTip').css('top', '');
|
||||||
|
window.setTimeout(function () {
|
||||||
|
$('#loadingTip').fadeOut(750);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
UI.errorLoadingScreen = function (error, transparent) {
|
||||||
|
if (!$('#' + LOADING).is(':visible')) { UI.addLoadingScreen(undefined, true); }
|
||||||
|
$('.spinnerContainer').hide();
|
||||||
|
if (transparent) { $('#' + LOADING).css('opacity', 0.8); }
|
||||||
|
$('#' + LOADING).find('p').html(error || Messages.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
var notify = {};
|
||||||
|
UI.unnotify = function () {
|
||||||
|
if (notify.tabNotification &&
|
||||||
|
typeof(notify.tabNotification.cancel) === 'function') {
|
||||||
|
notify.tabNotification.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
UI.notify = function () {
|
||||||
|
if (Visible.isSupported() && !Visible.currently()) {
|
||||||
|
UI.unnotify();
|
||||||
|
notify.tabNotification = Notify.tab(1000, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Visible.isSupported()) {
|
||||||
|
Visible.onChange(function (yes) {
|
||||||
|
if (yes) { UI.unnotify(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
UI.importContent = function (type, f) {
|
||||||
|
return function () {
|
||||||
|
var $files = $('<input type="file">').click();
|
||||||
|
$files.on('change', function (e) {
|
||||||
|
var file = e.target.files[0];
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (e) { f(e.target.result, file); };
|
||||||
|
reader.readAsText(file, type);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return UI;
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
define(function () {
|
||||||
|
var module = {};
|
||||||
|
|
||||||
|
module.create = function (UserList, Title, cfg) {
|
||||||
|
var exp = {};
|
||||||
|
|
||||||
|
exp.update = function (shjson) {
|
||||||
|
// Extract the user list (metadata) from the hyperjson
|
||||||
|
var json = (!shjson || typeof shjson !== "string") ? "" : JSON.parse(shjson);
|
||||||
|
var titleUpdated = false;
|
||||||
|
var metadata;
|
||||||
|
if (Array.isArray(json)) {
|
||||||
|
metadata = json[3] && json[3].metadata;
|
||||||
|
} else {
|
||||||
|
metadata = json.metadata;
|
||||||
|
}
|
||||||
|
if (typeof metadata === "object") {
|
||||||
|
if (metadata.users) {
|
||||||
|
var userData = metadata.users;
|
||||||
|
// Update the local user data
|
||||||
|
UserList.addToUserData(userData);
|
||||||
|
}
|
||||||
|
if (metadata.defaultTitle) {
|
||||||
|
Title.updateDefaultTitle(metadata.defaultTitle);
|
||||||
|
}
|
||||||
|
if (typeof metadata.title !== "undefined") {
|
||||||
|
Title.updateTitle(metadata.title || Title.defaultTitle);
|
||||||
|
titleUpdated = true;
|
||||||
|
}
|
||||||
|
if (metadata.slideOptions && cfg.slideOptions) {
|
||||||
|
cfg.slideOptions(metadata.slideOptions);
|
||||||
|
}
|
||||||
|
if (metadata.color && cfg.slideColors) {
|
||||||
|
cfg.slideColors(metadata.color, metadata.backColor);
|
||||||
|
}
|
||||||
|
if (typeof(metadata.palette) !== 'undefined' && cfg.updatePalette) {
|
||||||
|
cfg.updatePalette(metadata.palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!titleUpdated) {
|
||||||
|
Title.updateTitle(Title.defaultTitle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return exp;
|
||||||
|
};
|
||||||
|
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
|||||||
|
define(function () {
|
||||||
|
var module = {};
|
||||||
|
|
||||||
|
module.create = function (cfg, onLocal, Cryptpad) {
|
||||||
|
var exp = {};
|
||||||
|
|
||||||
|
var parsed = exp.parsedHref = Cryptpad.parsePadUrl(window.location.href);
|
||||||
|
exp.defaultTitle = Cryptpad.getDefaultName(parsed);
|
||||||
|
|
||||||
|
exp.title = document.title; // TOOD slides
|
||||||
|
|
||||||
|
cfg = cfg || {};
|
||||||
|
|
||||||
|
var getHeadingText = cfg.getHeadingText || function () { return; };
|
||||||
|
var updateLocalTitle = function (newTitle) {
|
||||||
|
exp.title = newTitle;
|
||||||
|
if (typeof cfg.updateLocalTitle === "function") {
|
||||||
|
cfg.updateLocalTitle(newTitle);
|
||||||
|
} else {
|
||||||
|
document.title = newTitle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var $title;
|
||||||
|
exp.setToolbar = function (toolbar) {
|
||||||
|
$title = toolbar && toolbar.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.getTitle = function () { return exp.title; };
|
||||||
|
var isDefaultTitle = exp.isDefaultTitle = function (){return exp.title === exp.defaultTitle;};
|
||||||
|
|
||||||
|
var suggestTitle = exp.suggestTitle = function (fallback) {
|
||||||
|
if (isDefaultTitle()) {
|
||||||
|
return getHeadingText() || fallback || "";
|
||||||
|
} else {
|
||||||
|
return exp.title || getHeadingText() || exp.defaultTitle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var renameCb = function (err, newTitle) {
|
||||||
|
if (err) { return; }
|
||||||
|
updateLocalTitle(newTitle);
|
||||||
|
console.log('here');
|
||||||
|
onLocal();
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.updateTitle = function (newTitle) {
|
||||||
|
if (newTitle === exp.title) { return; }
|
||||||
|
// Change the title now, and set it back to the old value if there is an error
|
||||||
|
var oldTitle = exp.title;
|
||||||
|
Cryptpad.renamePad(newTitle, function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
console.log("Couldn't set pad title");
|
||||||
|
console.error(err);
|
||||||
|
updateLocalTitle(oldTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateLocalTitle(data);
|
||||||
|
if (!$title) { return; }
|
||||||
|
$title.find('span.title').text(data);
|
||||||
|
$title.find('input').val(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.updateDefaultTitle = function (newDefaultTitle) {
|
||||||
|
exp.defaultTitle = newDefaultTitle;
|
||||||
|
if (!$title) { return; }
|
||||||
|
$title.find('input').attr("placeholder", exp.defaultTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.getTitleConfig = function () {
|
||||||
|
return {
|
||||||
|
onRename: renameCb,
|
||||||
|
suggestName: suggestTitle,
|
||||||
|
defaultName: exp.defaultTitle
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return exp;
|
||||||
|
};
|
||||||
|
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,105 @@
|
|||||||
|
define(function () {
|
||||||
|
var module = {};
|
||||||
|
|
||||||
|
module.create = function (info, onLocal, Cryptget, Cryptpad) {
|
||||||
|
var exp = {};
|
||||||
|
|
||||||
|
var userData = exp.userData = {};
|
||||||
|
var userList = exp.userList = info.userList;
|
||||||
|
var myData = exp.myData = {};
|
||||||
|
exp.myUserName = info.myID;
|
||||||
|
exp.myNetfluxId = info.myID;
|
||||||
|
|
||||||
|
var network = Cryptpad.getNetwork();
|
||||||
|
|
||||||
|
var parsed = Cryptpad.parsePadUrl(window.location.href);
|
||||||
|
var appType = parsed ? parsed.type : undefined;
|
||||||
|
|
||||||
|
var addToUserData = exp.addToUserData = function(data) {
|
||||||
|
var users = userList.users;
|
||||||
|
for (var attrname in data) { userData[attrname] = data[attrname]; }
|
||||||
|
|
||||||
|
if (users && users.length) {
|
||||||
|
for (var userKey in userData) {
|
||||||
|
if (users.indexOf(userKey) === -1) {
|
||||||
|
delete userData[userKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(userList && typeof userList.onChange === "function") {
|
||||||
|
userList.onChange(userData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.getToolbarConfig = function () {
|
||||||
|
return {
|
||||||
|
data: userData,
|
||||||
|
list: userList,
|
||||||
|
userNetfluxId: exp.myNetfluxId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var setName = exp.setName = function (newName, cb) {
|
||||||
|
if (typeof(newName) !== 'string') { return; }
|
||||||
|
var myUserNameTemp = newName.trim();
|
||||||
|
if(myUserNameTemp.length > 32) {
|
||||||
|
myUserNameTemp = myUserNameTemp.substr(0, 32);
|
||||||
|
}
|
||||||
|
exp.myUserName = myUserNameTemp;
|
||||||
|
myData = {};
|
||||||
|
myData[exp.myNetfluxId] = {
|
||||||
|
name: exp.myUserName,
|
||||||
|
uid: Cryptpad.getUid(),
|
||||||
|
};
|
||||||
|
addToUserData(myData);
|
||||||
|
Cryptpad.setAttribute('username', exp.myUserName, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.log("Couldn't set username");
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof cb === "function") { cb(); }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.getLastName = function ($changeNameButton, isNew) {
|
||||||
|
Cryptpad.getLastName(function (err, lastName) {
|
||||||
|
if (err) {
|
||||||
|
console.log("Could not get previous name");
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update the toolbar list:
|
||||||
|
// Add the current user in the metadata
|
||||||
|
if (typeof(lastName) === 'string') {
|
||||||
|
setName(lastName, onLocal);
|
||||||
|
} else {
|
||||||
|
myData[exp.myNetfluxId] = {
|
||||||
|
name: "",
|
||||||
|
uid: Cryptpad.getUid(),
|
||||||
|
};
|
||||||
|
addToUserData(myData);
|
||||||
|
onLocal();
|
||||||
|
$changeNameButton.click();
|
||||||
|
}
|
||||||
|
if (isNew && appType) {
|
||||||
|
Cryptpad.selectTemplate(appType, info.realtime, Cryptget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Cryptpad.onDisplayNameChanged(function (newName) {
|
||||||
|
setName(newName, onLocal);
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on('reconnect', function (uid) {
|
||||||
|
exp.myNetfluxId = uid;
|
||||||
|
exp.setName(exp.myUserName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return exp;
|
||||||
|
};
|
||||||
|
|
||||||
|
return module;
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,127 @@
|
|||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/bower_components/marked/marked.min.js',
|
||||||
|
'/bower_components/diff-dom/diffDOM.js'
|
||||||
|
],function ($, Marked) {
|
||||||
|
var DiffMd = {};
|
||||||
|
|
||||||
|
var DiffDOM = window.diffDOM;
|
||||||
|
var renderer = new Marked.Renderer();
|
||||||
|
|
||||||
|
Marked.setOptions({
|
||||||
|
renderer: renderer
|
||||||
|
});
|
||||||
|
|
||||||
|
DiffMd.render = function (md) {
|
||||||
|
return Marked(md);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tasks list
|
||||||
|
var checkedTaskItemPtn = /^\s*\[x\]\s*/;
|
||||||
|
var uncheckedTaskItemPtn = /^\s*\[ \]\s*/;
|
||||||
|
renderer.listitem = function (text) {
|
||||||
|
var isCheckedTaskItem = checkedTaskItemPtn.test(text);
|
||||||
|
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
|
||||||
|
if (isCheckedTaskItem) {
|
||||||
|
text = text.replace(checkedTaskItemPtn,
|
||||||
|
'<i class="fa fa-check-square" aria-hidden="true"></i> ') + '\n';
|
||||||
|
}
|
||||||
|
if (isUncheckedTaskItem) {
|
||||||
|
text = text.replace(uncheckedTaskItemPtn,
|
||||||
|
'<i class="fa fa-square-o" aria-hidden="true"></i> ') + '\n';
|
||||||
|
}
|
||||||
|
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
|
||||||
|
return '<li'+ cls + '>' + text + '</li>\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
var forbiddenTags = [
|
||||||
|
'SCRIPT',
|
||||||
|
'IFRAME',
|
||||||
|
'OBJECT',
|
||||||
|
'APPLET',
|
||||||
|
'VIDEO',
|
||||||
|
'AUDIO',
|
||||||
|
];
|
||||||
|
var unsafeTag = function (info) {
|
||||||
|
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
|
||||||
|
if (/^on/.test(info.diff.name)) {
|
||||||
|
console.log("Rejecting forbidden element attribute with name", info.diff.name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
|
||||||
|
var msg = "Rejecting forbidden tag of type (%s)";
|
||||||
|
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
|
||||||
|
console.log(msg, info.diff.element.nodeName);
|
||||||
|
return true;
|
||||||
|
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeName) !== -1) {
|
||||||
|
console.log("Replacing restricted element type (%s) with PRE", info.diff.newValue.nodeName);
|
||||||
|
info.diff.newValue.nodeName = 'PRE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var slice = function (coll) {
|
||||||
|
return Array.prototype.slice.call(coll);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* remove listeners from the DOM */
|
||||||
|
var removeListeners = function (root) {
|
||||||
|
slice(root.attributes).map(function (attr) {
|
||||||
|
if (/^on/.test(attr.name)) {
|
||||||
|
root.attributes.removeNamedItem(attr.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// all the way down
|
||||||
|
slice(root.children).forEach(removeListeners);
|
||||||
|
};
|
||||||
|
|
||||||
|
var domFromHTML = function (html) {
|
||||||
|
var Dom = new DOMParser().parseFromString(html, "text/html");
|
||||||
|
removeListeners(Dom.body);
|
||||||
|
return Dom;
|
||||||
|
};
|
||||||
|
|
||||||
|
var DD = new DiffDOM({
|
||||||
|
preDiffApply: function (info) {
|
||||||
|
if (unsafeTag(info)) { return true; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var makeDiff = function (A, B, id) {
|
||||||
|
var Err;
|
||||||
|
var Els = [A, B].map(function (frag) {
|
||||||
|
if (typeof(frag) === 'object') {
|
||||||
|
if (!frag || (frag && !frag.body)) {
|
||||||
|
Err = "No body";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var els = frag.body.querySelectorAll('#'+id);
|
||||||
|
if (els.length) {
|
||||||
|
return els[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err = 'No candidate found';
|
||||||
|
});
|
||||||
|
if (Err) { return Err; }
|
||||||
|
var patch = DD.diff(Els[0], Els[1]);
|
||||||
|
return patch;
|
||||||
|
};
|
||||||
|
|
||||||
|
DiffMd.apply = function (newHtml, $content) {
|
||||||
|
var id = $content.attr('id');
|
||||||
|
if (!id) { throw new Error("The element must have a valid id"); }
|
||||||
|
var $div = $('<div>', {id: id}).append(newHtml);
|
||||||
|
var Dom = domFromHTML($('<div>').append($div).html());
|
||||||
|
var oldDom = domFromHTML($content[0].outerHTML);
|
||||||
|
var patch = makeDiff(oldDom, Dom, id);
|
||||||
|
if (typeof(patch) === 'string') {
|
||||||
|
throw new Error(patch);
|
||||||
|
} else {
|
||||||
|
DD.apply($content[0], patch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return DiffMd;
|
||||||
|
});
|
||||||
|
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,905 @@
|
|||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/customize/application_config.js',
|
||||||
|
'/api/config'
|
||||||
|
], function ($, Config, ApiConfig) {
|
||||||
|
var Messages = {};
|
||||||
|
var Cryptpad;
|
||||||
|
|
||||||
|
var Bar = {
|
||||||
|
constants: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
var SPINNER_DISAPPEAR_TIME = 3000;
|
||||||
|
|
||||||
|
// Toolbar parts
|
||||||
|
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
|
||||||
|
var TOP_CLS = Bar.constants.top = 'cryptpad-toolbar-top';
|
||||||
|
var LEFTSIDE_CLS = Bar.constants.leftside = 'cryptpad-toolbar-leftside';
|
||||||
|
var RIGHTSIDE_CLS = Bar.constants.rightside = 'cryptpad-toolbar-rightside';
|
||||||
|
var HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history';
|
||||||
|
|
||||||
|
// Userlist
|
||||||
|
var USERLIST_CLS = Bar.constants.userlist = "cryptpad-dropdown-users";
|
||||||
|
var EDITSHARE_CLS = Bar.constants.editShare = "cryptpad-dropdown-editShare";
|
||||||
|
var VIEWSHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-viewShare";
|
||||||
|
var SHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-share";
|
||||||
|
|
||||||
|
// Top parts
|
||||||
|
var USER_CLS = Bar.constants.userAdmin = "cryptpad-user";
|
||||||
|
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner';
|
||||||
|
var STATE_CLS = Bar.constants.state = 'cryptpad-state';
|
||||||
|
var LAG_CLS = Bar.constants.lag = 'cryptpad-lag';
|
||||||
|
var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit';
|
||||||
|
var TITLE_CLS = Bar.constants.title = "cryptpad-title";
|
||||||
|
var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-newpad";
|
||||||
|
|
||||||
|
// User admin menu
|
||||||
|
var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown';
|
||||||
|
var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username';
|
||||||
|
var READONLY_CLS = Bar.constants.readonly = 'cryptpad-readonly';
|
||||||
|
var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username";
|
||||||
|
|
||||||
|
// Create the toolbar element
|
||||||
|
|
||||||
|
var uid = function () {
|
||||||
|
return 'cryptpad-uid-' + String(Math.random()).substring(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
var styleToolbar = function ($container, href, version) {
|
||||||
|
href = href || '/customize/toolbar.css' + (version?('?' + version): '');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: href,
|
||||||
|
dataType: 'text',
|
||||||
|
success: function (data) {
|
||||||
|
$container.append($('<style>').text(data));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var createRealtimeToolbar = function (config) {
|
||||||
|
if (!config.$container) { return; }
|
||||||
|
var $container = config.$container;
|
||||||
|
var $toolbar = $('<div>', {
|
||||||
|
'class': TOOLBAR_CLS,
|
||||||
|
id: uid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
var $topContainer = $('<div>', {'class': TOP_CLS});
|
||||||
|
var $userContainer = $('<span>', {
|
||||||
|
'class': USER_CLS
|
||||||
|
}).appendTo($topContainer);
|
||||||
|
$('<span>', {'class': SPINNER_CLS}).hide().appendTo($userContainer);
|
||||||
|
$('<span>', {'class': STATE_CLS}).hide().appendTo($userContainer);
|
||||||
|
$('<span>', {'class': LAG_CLS}).hide().appendTo($userContainer);
|
||||||
|
$('<span>', {'class': LIMIT_CLS}).hide().appendTo($userContainer);
|
||||||
|
$('<span>', {'class': NEWPAD_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
|
||||||
|
$('<span>', {'class': USERADMIN_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
|
||||||
|
|
||||||
|
$toolbar.append($topContainer)
|
||||||
|
.append($('<div>', {'class': LEFTSIDE_CLS}))
|
||||||
|
.append($('<div>', {'class': RIGHTSIDE_CLS}))
|
||||||
|
.append($('<div>', {'class': HISTORY_CLS}));
|
||||||
|
|
||||||
|
// The 'notitle' class removes the line added for the title with a small screen
|
||||||
|
if (!config.title || typeof config.title !== "object") {
|
||||||
|
$toolbar.addClass('notitle');
|
||||||
|
}
|
||||||
|
|
||||||
|
$container.prepend($toolbar);
|
||||||
|
|
||||||
|
if (ApiConfig && ApiConfig.requireConf && ApiConfig.requireConf.urlArgs) {
|
||||||
|
styleToolbar($container, undefined, ApiConfig.requireConf.urlArgs);
|
||||||
|
} else {
|
||||||
|
styleToolbar($container);
|
||||||
|
}
|
||||||
|
return $toolbar;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Userlist elements
|
||||||
|
|
||||||
|
var checkSynchronizing = function (toolbar, config) {
|
||||||
|
if (!toolbar.state) { return; }
|
||||||
|
var userList = config.userList.list.users;
|
||||||
|
var userNetfluxId = config.userList.userNetfluxId;
|
||||||
|
var meIdx = userList.indexOf(userNetfluxId);
|
||||||
|
if (meIdx === -1) {
|
||||||
|
toolbar.state.text(Messages.synchronizing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toolbar.state.text('');
|
||||||
|
};
|
||||||
|
var getOtherUsers = function(config) {
|
||||||
|
var userList = config.userList.list.users;
|
||||||
|
var userData = config.userList.data;
|
||||||
|
var userNetfluxId = config.userList.userNetfluxId;
|
||||||
|
|
||||||
|
var i = 0; // duplicates counter
|
||||||
|
var list = [];
|
||||||
|
|
||||||
|
// Display only one time each user (if he is connected in multiple tabs)
|
||||||
|
var myUid = userData[userNetfluxId] ? userData[userNetfluxId].uid : undefined;
|
||||||
|
var uids = [];
|
||||||
|
userList.forEach(function(user) {
|
||||||
|
if (user !== userNetfluxId) {
|
||||||
|
var data = userData[user] || {};
|
||||||
|
var userName = data.name;
|
||||||
|
var userId = data.uid;
|
||||||
|
if (userName && uids.indexOf(userId) === -1 && (!myUid || userId !== myUid)) {
|
||||||
|
uids.push(userId);
|
||||||
|
list.push(userName);
|
||||||
|
} else if (userName) { i++; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
list: list,
|
||||||
|
duplicates: i
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var arrayIntersect = function(a, b) {
|
||||||
|
return $.grep(a, function(i) {
|
||||||
|
return $.inArray(i, b) > -1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var updateUserList = function (toolbar, config) {
|
||||||
|
// Make sure the elements are displayed
|
||||||
|
var $userButtons = toolbar.userlist;
|
||||||
|
|
||||||
|
var userList = config.userList.list.users;
|
||||||
|
var userData = config.userList.data;
|
||||||
|
var userNetfluxId = config.userList.userNetfluxId;
|
||||||
|
|
||||||
|
var numberOfUsers = userList.length;
|
||||||
|
|
||||||
|
// If we are using old pads (readonly unavailable), only editing users are in userList.
|
||||||
|
// With new pads, we also have readonly users in userList, so we have to intersect with
|
||||||
|
// the userData to have only the editing users. We can't use userData directly since it
|
||||||
|
// may contain data about users that have already left the channel.
|
||||||
|
userList = config.readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData));
|
||||||
|
|
||||||
|
// Names of editing users
|
||||||
|
var others = getOtherUsers(config);
|
||||||
|
var editUsersNames = others.list;
|
||||||
|
var duplicates = others.duplicates; // Number of duplicates
|
||||||
|
|
||||||
|
var numberOfEditUsers = userList.length - duplicates;
|
||||||
|
var numberOfViewUsers = numberOfUsers - userList.length;
|
||||||
|
|
||||||
|
// Number of anonymous editing users
|
||||||
|
var anonymous = numberOfEditUsers - editUsersNames.length;
|
||||||
|
|
||||||
|
// Update the userlist
|
||||||
|
var $usersTitle = $('<h2>').text(Messages.users);
|
||||||
|
var $editUsers = $userButtons.find('.' + USERLIST_CLS);
|
||||||
|
$editUsers.html('').append($usersTitle);
|
||||||
|
|
||||||
|
var $editUsersList = $('<pre>');
|
||||||
|
// Yourself (edit only)
|
||||||
|
if (config.readOnly !== 1) {
|
||||||
|
$editUsers.append('<span class="yourself">' + Messages.yourself + '</span>');
|
||||||
|
anonymous--;
|
||||||
|
}
|
||||||
|
// Editors
|
||||||
|
$editUsersList.text(editUsersNames.join('\n')); // .text() to avoid XSS
|
||||||
|
$editUsers.append($editUsersList);
|
||||||
|
// Anonymous editors
|
||||||
|
if (anonymous > 0) {
|
||||||
|
var text = anonymous === 1 ? Messages.anonymousUser : Messages.anonymousUsers;
|
||||||
|
$editUsers.append('<span class="anonymous">' + anonymous + ' ' + text + '</span>');
|
||||||
|
}
|
||||||
|
// Viewers
|
||||||
|
if (numberOfViewUsers > 0) {
|
||||||
|
var viewText = '<span class="viewer">';
|
||||||
|
if (numberOfEditUsers > 0) {
|
||||||
|
$editUsers.append('<br>');
|
||||||
|
viewText += Messages.and + ' ';
|
||||||
|
}
|
||||||
|
var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer;
|
||||||
|
viewText += numberOfViewUsers + ' ' + viewerText + '</span>';
|
||||||
|
$editUsers.append(viewText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the buttons
|
||||||
|
var fa_editusers = '<span class="fa fa-users"></span>';
|
||||||
|
var fa_viewusers = '<span class="fa fa-eye"></span>';
|
||||||
|
var viewersText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer;
|
||||||
|
var editorsText = numberOfEditUsers !== 1 ? Messages.editors : Messages.editor;
|
||||||
|
var $span = $('<span>', {'class': 'large'}).html(fa_editusers + ' ' + numberOfEditUsers + ' ' + editorsText + ' ' + fa_viewusers + ' ' + numberOfViewUsers + ' ' + viewersText);
|
||||||
|
var $spansmall = $('<span>', {'class': 'narrow'}).html(fa_editusers + ' ' + numberOfEditUsers + ' ' + fa_viewusers + ' ' + numberOfViewUsers);
|
||||||
|
$userButtons.find('.buttonTitle').html('').append($span).append($spansmall);
|
||||||
|
|
||||||
|
// Change username in useradmin dropdown
|
||||||
|
if (config.displayed.indexOf('useradmin') !== -1) {
|
||||||
|
var $userAdminElement = toolbar.$userAdmin;
|
||||||
|
var $userElement = $userAdminElement.find('.' + USERNAME_CLS);
|
||||||
|
$userElement.show();
|
||||||
|
if (config.readOnly === 1) {
|
||||||
|
$userElement.addClass(READONLY_CLS).text(Messages.readonly);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var name = userData[userNetfluxId] && userData[userNetfluxId].name;
|
||||||
|
if (!name) {
|
||||||
|
name = Messages.anonymous;
|
||||||
|
}
|
||||||
|
$userElement.removeClass(READONLY_CLS).text(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var initUserList = function (toolbar, config) {
|
||||||
|
if (config.userList && config.userList.list && config.userList.userNetfluxId) {
|
||||||
|
var userList = config.userList.list;
|
||||||
|
userList.change.push(function () {
|
||||||
|
var users = userList.users;
|
||||||
|
if (users.indexOf(config.userList.userNetfluxId) !== -1) {toolbar.connected = true;}
|
||||||
|
if (!toolbar.connected) { return; }
|
||||||
|
checkSynchronizing(toolbar, config);
|
||||||
|
if (config.userList.data) {
|
||||||
|
updateUserList(toolbar, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Create sub-elements
|
||||||
|
|
||||||
|
var createUserList = function (toolbar, config) {
|
||||||
|
if (!config.userList || !config.userList.list ||
|
||||||
|
!config.userList.data || !config.userList.userNetfluxId) {
|
||||||
|
throw new Error("You must provide a `userList` object to display the userlist");
|
||||||
|
}
|
||||||
|
var dropdownConfig = {
|
||||||
|
options: [{
|
||||||
|
tag: 'p',
|
||||||
|
attributes: {'class': USERLIST_CLS},
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
var $block = Cryptpad.createDropdown(dropdownConfig);
|
||||||
|
$block.attr('id', 'userButtons');
|
||||||
|
toolbar.$leftside.prepend($block);
|
||||||
|
|
||||||
|
return $block;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createShare = function (toolbar, config) {
|
||||||
|
var secret = Cryptpad.find(config, ['share', 'secret']);
|
||||||
|
var channel = Cryptpad.find(config, ['share', 'channel']);
|
||||||
|
if (!secret || !channel) {
|
||||||
|
throw new Error("Unable to display the share button: share.secret and share.channel required");
|
||||||
|
}
|
||||||
|
Cryptpad.getRecentPads(function (err, recent) {
|
||||||
|
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
|
||||||
|
var $span = $('<span>', {'class': 'large'}).append(' ' +Messages.shareButton);
|
||||||
|
var hashes = Cryptpad.getHashes(channel, secret);
|
||||||
|
var options = [];
|
||||||
|
|
||||||
|
// If we have a stronger version in drive, add it and add a redirect button
|
||||||
|
var stronger = recent && Cryptpad.findStronger(null, recent);
|
||||||
|
if (stronger) {
|
||||||
|
var parsed = Cryptpad.parsePadUrl(stronger);
|
||||||
|
hashes.editHash = parsed.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashes.editHash) {
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {title: Messages.editShareTitle, 'class': 'editShare'},
|
||||||
|
content: '<span class="fa fa-users"></span> ' + Messages.editShare
|
||||||
|
});
|
||||||
|
if (stronger) {
|
||||||
|
// We're in view mode, display the "open editing link" button
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {
|
||||||
|
title: Messages.editOpenTitle,
|
||||||
|
'class': 'editOpen',
|
||||||
|
href: window.location.pathname + '#' + hashes.editHash,
|
||||||
|
target: '_blank'
|
||||||
|
},
|
||||||
|
content: '<span class="fa fa-users"></span> ' + Messages.editOpen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
options.push({tag: 'hr'});
|
||||||
|
}
|
||||||
|
if (hashes.viewHash) {
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {title: Messages.viewShareTitle, 'class': 'viewShare'},
|
||||||
|
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
|
||||||
|
});
|
||||||
|
if (hashes.editHash && !stronger) {
|
||||||
|
// We're in edit mode, display the "open readonly" button
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {
|
||||||
|
title: Messages.viewOpenTitle,
|
||||||
|
'class': 'viewOpen',
|
||||||
|
href: window.location.pathname + '#' + hashes.viewHash,
|
||||||
|
target: '_blank'
|
||||||
|
},
|
||||||
|
content: '<span class="fa fa-eye"></span> ' + Messages.viewOpen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hashes.fileHash) {
|
||||||
|
options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {title: Messages.viewShareTitle, 'class': 'fileShare'},
|
||||||
|
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var dropdownConfigShare = {
|
||||||
|
text: $('<div>').append($shareIcon).append($span).html(),
|
||||||
|
options: options
|
||||||
|
};
|
||||||
|
var $shareBlock = Cryptpad.createDropdown(dropdownConfigShare);
|
||||||
|
$shareBlock.find('button').attr('id', 'shareButton');
|
||||||
|
$shareBlock.find('.dropdown-bar-content').addClass(SHARE_CLS).addClass(EDITSHARE_CLS).addClass(VIEWSHARE_CLS);
|
||||||
|
|
||||||
|
if (hashes.editHash) {
|
||||||
|
$shareBlock.find('a.editShare').click(function () {
|
||||||
|
var url = window.location.origin + window.location.pathname + '#' + hashes.editHash;
|
||||||
|
var success = Cryptpad.Clipboard.copy(url);
|
||||||
|
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (hashes.viewHash) {
|
||||||
|
$shareBlock.find('a.viewShare').click(function () {
|
||||||
|
var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash ;
|
||||||
|
var success = Cryptpad.Clipboard.copy(url);
|
||||||
|
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (hashes.fileHash) {
|
||||||
|
$shareBlock.find('a.fileShare').click(function () {
|
||||||
|
var url = window.location.origin + window.location.pathname + '#' + hashes.fileHash ;
|
||||||
|
var success = Cryptpad.Clipboard.copy(url);
|
||||||
|
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.$leftside.append($shareBlock);
|
||||||
|
toolbar.share = $shareBlock;
|
||||||
|
});
|
||||||
|
|
||||||
|
return "Loading share button";
|
||||||
|
};
|
||||||
|
|
||||||
|
var createFileShare = function (toolbar) {
|
||||||
|
if (!window.location.hash) {
|
||||||
|
throw new Error("Unable to display the share button: hash required in the URL");
|
||||||
|
}
|
||||||
|
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
|
||||||
|
var $span = $('<span>', {'class': 'large'}).append(' ' +Messages.shareButton);
|
||||||
|
var $button = $('<button>', {'id': 'shareButton'}).append($shareIcon).append($span);
|
||||||
|
$button.click(function () {
|
||||||
|
var url = window.location.href;
|
||||||
|
var success = Cryptpad.Clipboard.copy(url);
|
||||||
|
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbar.$leftside.append($button);
|
||||||
|
return $button;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createTitle = function (toolbar, config) {
|
||||||
|
var $titleContainer = $('<span>', {
|
||||||
|
id: 'toolbarTitle',
|
||||||
|
'class': TITLE_CLS
|
||||||
|
}).appendTo(toolbar.$top);
|
||||||
|
|
||||||
|
// TODO: move these functions to toolbar or common?
|
||||||
|
if (typeof config.title !== "object") {
|
||||||
|
console.error("config.title", config);
|
||||||
|
throw new Error("config.title is not an object");
|
||||||
|
}
|
||||||
|
var callback = config.title.onRename;
|
||||||
|
var placeholder = config.title.defaultName;
|
||||||
|
var suggestName = config.title.suggestName;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
var $text = $('<span>', {
|
||||||
|
'class': 'title'
|
||||||
|
}).appendTo($titleContainer);
|
||||||
|
var $pencilIcon = $('<span>', {
|
||||||
|
'class': 'pencilIcon',
|
||||||
|
'title': Messages.clickToEdit
|
||||||
|
});
|
||||||
|
if (config.readOnly === 1 || typeof(Cryptpad) === "undefined") { return $titleContainer; }
|
||||||
|
var $input = $('<input>', {
|
||||||
|
type: 'text',
|
||||||
|
placeholder: placeholder
|
||||||
|
}).appendTo($titleContainer).hide();
|
||||||
|
if (config.readOnly !== 1) {
|
||||||
|
$text.attr("title", Messages.clickToEdit);
|
||||||
|
$text.addClass("editable");
|
||||||
|
var $icon = $('<span>', {
|
||||||
|
'class': 'fa fa-pencil readonly',
|
||||||
|
style: 'font-family: FontAwesome;'
|
||||||
|
});
|
||||||
|
$pencilIcon.append($icon).appendTo($titleContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
$input.on('mousedown', function (e) {
|
||||||
|
if (!$input.is(":focus")) {
|
||||||
|
$input.focus();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
$input.on('keyup', function (e) {
|
||||||
|
if (e.which === 13 && toolbar.connected === true) {
|
||||||
|
var name = $input.val().trim();
|
||||||
|
if (name === "") {
|
||||||
|
name = $input.attr('placeholder');
|
||||||
|
}
|
||||||
|
Cryptpad.renamePad(name, function (err, newtitle) {
|
||||||
|
if (err) { return; }
|
||||||
|
$text.text(newtitle);
|
||||||
|
callback(null, newtitle);
|
||||||
|
$input.hide();
|
||||||
|
$text.show();
|
||||||
|
//$pencilIcon.css('display', '');
|
||||||
|
});
|
||||||
|
} else if (e.which === 27) {
|
||||||
|
$input.hide();
|
||||||
|
$text.show();
|
||||||
|
//$pencilIcon.css('display', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var displayInput = function () {
|
||||||
|
if (toolbar.connected === false) { return; }
|
||||||
|
$text.hide();
|
||||||
|
//$pencilIcon.css('display', 'none');
|
||||||
|
var inputVal = suggestName() || "";
|
||||||
|
$input.val(inputVal);
|
||||||
|
$input.show();
|
||||||
|
$input.focus();
|
||||||
|
};
|
||||||
|
$text.on('click', displayInput);
|
||||||
|
$pencilIcon.on('click', displayInput);
|
||||||
|
return $titleContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createLinkToMain = function (toolbar) {
|
||||||
|
var $linkContainer = $('<span>', {
|
||||||
|
'class': "cryptpad-link"
|
||||||
|
}).appendTo(toolbar.$top);
|
||||||
|
var $imgTag = $('<img>', {
|
||||||
|
src: "/customize/cryptofist_mini.png",
|
||||||
|
alt: "Cryptpad"
|
||||||
|
});
|
||||||
|
|
||||||
|
// We need to override the "a" tag action here because it is inside the iframe!
|
||||||
|
var $aTagSmall = $('<a>', {
|
||||||
|
href: "/",
|
||||||
|
title: Messages.header_logoTitle,
|
||||||
|
'class': "cryptpad-logo"
|
||||||
|
}).append($imgTag);
|
||||||
|
var $span = $('<span>').text('CryptPad');
|
||||||
|
var $aTagBig = $aTagSmall.clone().addClass('large').append($span);
|
||||||
|
$aTagSmall.addClass('narrow');
|
||||||
|
var onClick = function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
window.open('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location = "/";
|
||||||
|
};
|
||||||
|
|
||||||
|
var onContext = function (e) { e.stopPropagation(); };
|
||||||
|
|
||||||
|
$aTagBig.click(onClick).contextmenu(onContext);
|
||||||
|
$aTagSmall.click(onClick).contextmenu(onContext);
|
||||||
|
|
||||||
|
$linkContainer.append($aTagSmall).append($aTagBig);
|
||||||
|
|
||||||
|
return $linkContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
var checkLag = function (toolbar, config, $lagEl) {
|
||||||
|
var lag;
|
||||||
|
var $lag = $lagEl || toolbar.lag;
|
||||||
|
if (!$lag) { return; }
|
||||||
|
var getLag = config.network.getLag;
|
||||||
|
if(typeof getLag === "function") {
|
||||||
|
lag = getLag();
|
||||||
|
}
|
||||||
|
var lagLight = $('<div>', {
|
||||||
|
'class': 'lag'
|
||||||
|
});
|
||||||
|
var title;
|
||||||
|
if (lag && toolbar.connected) {
|
||||||
|
$lag.attr('class', LAG_CLS);
|
||||||
|
toolbar.firstConnection = false;
|
||||||
|
title = Messages.lag + ' : ' + lag + ' ms\n';
|
||||||
|
if (lag > 30000) {
|
||||||
|
$lag.addClass('lag0');
|
||||||
|
title = Messages.redLight;
|
||||||
|
} else if (lag > 5000) {
|
||||||
|
$lag.addClass('lag1');
|
||||||
|
title += Messages.orangeLight;
|
||||||
|
} else if (lag > 1000) {
|
||||||
|
$lag.addClass('lag2');
|
||||||
|
title += Messages.orangeLight;
|
||||||
|
} else if (lag > 300) {
|
||||||
|
$lag.addClass('lag3');
|
||||||
|
title += Messages.greenLight;
|
||||||
|
} else {
|
||||||
|
$lag.addClass('lag4');
|
||||||
|
title += Messages.greenLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!toolbar.firstConnection) {
|
||||||
|
$lag.attr('class', LAG_CLS);
|
||||||
|
// Display the red light at the 2nd failed attemp to get the lag
|
||||||
|
lagLight.addClass('lag-red');
|
||||||
|
title = Messages.redLight;
|
||||||
|
}
|
||||||
|
if (title) {
|
||||||
|
$lag.attr('title', title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var createLag = function (toolbar, config) {
|
||||||
|
var $a = toolbar.$userAdmin.find('.'+LAG_CLS).show();
|
||||||
|
$('<span>', {'class': 'bar1'}).appendTo($a);
|
||||||
|
$('<span>', {'class': 'bar2'}).appendTo($a);
|
||||||
|
$('<span>', {'class': 'bar3'}).appendTo($a);
|
||||||
|
$('<span>', {'class': 'bar4'}).appendTo($a);
|
||||||
|
if (config.realtime) {
|
||||||
|
checkLag(toolbar, config, $a);
|
||||||
|
setInterval(function () {
|
||||||
|
if (!toolbar.connected) { return; }
|
||||||
|
checkLag(toolbar, config);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
return $a;
|
||||||
|
};
|
||||||
|
|
||||||
|
var kickSpinner = function (toolbar, config, local) {
|
||||||
|
if (!toolbar.spinner) { return; }
|
||||||
|
var $spin = toolbar.spinner;
|
||||||
|
$spin.find('.spin').show();
|
||||||
|
$spin.find('.synced').hide();
|
||||||
|
var onSynced = function () {
|
||||||
|
if ($spin.timeout) { clearTimeout($spin.timeout); }
|
||||||
|
$spin.timeout = setTimeout(function () {
|
||||||
|
$spin.find('.spin').hide();
|
||||||
|
$spin.find('.synced').show();
|
||||||
|
}, local ? 0 : SPINNER_DISAPPEAR_TIME);
|
||||||
|
};
|
||||||
|
if (Cryptpad) {
|
||||||
|
Cryptpad.whenRealtimeSyncs(config.realtime, onSynced);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSynced();
|
||||||
|
};
|
||||||
|
var ks = function (toolbar, config, local) {
|
||||||
|
return function () {
|
||||||
|
if (toolbar.connected) { kickSpinner(toolbar, config, local); }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var createSpinner = function (toolbar, config) {
|
||||||
|
var $spin = toolbar.$userAdmin.find('.'+SPINNER_CLS).show();
|
||||||
|
$('<span>', {
|
||||||
|
id: uid(),
|
||||||
|
'class': 'spin fa fa-spinner fa-pulse',
|
||||||
|
}).appendTo($spin).hide();
|
||||||
|
$('<span>', {
|
||||||
|
id: uid(),
|
||||||
|
'class': 'synced fa fa-check',
|
||||||
|
title: Messages.synced
|
||||||
|
}).appendTo($spin);
|
||||||
|
toolbar.$userAdmin.prepend($spin);
|
||||||
|
if (config.realtime) {
|
||||||
|
config.realtime.onPatch(ks(toolbar, config));
|
||||||
|
config.realtime.onMessage(ks(toolbar, config, true));
|
||||||
|
}
|
||||||
|
return $spin;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createState = function (toolbar) {
|
||||||
|
return toolbar.$userAdmin.find('.'+STATE_CLS).text(Messages.synchronizing).show();
|
||||||
|
};
|
||||||
|
|
||||||
|
var createLimit = function (toolbar) {
|
||||||
|
if (!Config.enablePinning) { return; }
|
||||||
|
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'});
|
||||||
|
var $limit = toolbar.$userAdmin.find('.'+LIMIT_CLS).attr({
|
||||||
|
'title': Messages.pinLimitReached
|
||||||
|
}).append($limitIcon).hide();
|
||||||
|
var todo = function (e, overLimit) {
|
||||||
|
if (e) { return void console.error("Unable to get the pinned usage"); }
|
||||||
|
if (overLimit) {
|
||||||
|
$limit.show().click(function () {
|
||||||
|
Cryptpad.alert(Messages._getKey('pinLimitReachedAlert', [encodeURIComponent(window.location.hostname)]), null, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Cryptpad.isOverPinLimit(todo);
|
||||||
|
return $limit;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createNewPad = function (toolbar) {
|
||||||
|
var $newPad = toolbar.$userAdmin.find('.'+NEWPAD_CLS).show();
|
||||||
|
|
||||||
|
var pads_options = [];
|
||||||
|
Config.availablePadTypes.forEach(function (p) {
|
||||||
|
if (p === 'drive') { return; }
|
||||||
|
pads_options.push({
|
||||||
|
tag: 'a',
|
||||||
|
attributes: {
|
||||||
|
'target': '_blank',
|
||||||
|
'href': '/' + p + '/',
|
||||||
|
},
|
||||||
|
content: Messages.type[p]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var $plusIcon = $('<span>', {'class': 'fa fa-plus'});
|
||||||
|
var $newbig = $('<span>', {'class': 'big'}).append(' ' +Messages.newButton);
|
||||||
|
var $newButton = $('<div>').append($plusIcon).append($newbig);
|
||||||
|
var dropdownConfig = {
|
||||||
|
text: $newButton.html(), // Button initial text
|
||||||
|
options: pads_options, // Entries displayed in the menu
|
||||||
|
left: true, // Open to the left of the button,
|
||||||
|
container: $newPad
|
||||||
|
};
|
||||||
|
var $newPadBlock = Cryptpad.createDropdown(dropdownConfig);
|
||||||
|
$newPadBlock.find('button').attr('title', Messages.newButtonTitle);
|
||||||
|
$newPadBlock.find('button').attr('id', 'newdoc');
|
||||||
|
return $newPadBlock;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createUserAdmin = function (toolbar, config) {
|
||||||
|
var $userAdmin = toolbar.$userAdmin.find('.'+USERADMIN_CLS).show();
|
||||||
|
var userMenuCfg = {
|
||||||
|
$initBlock: $userAdmin
|
||||||
|
};
|
||||||
|
if (!config.hideDisplayName) { // TODO: config.userAdmin.hideDisplayName?
|
||||||
|
$.extend(true, userMenuCfg, {
|
||||||
|
displayNameCls: USERNAME_CLS,
|
||||||
|
changeNameButtonCls: USERBUTTON_CLS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.readOnly !== 1) {
|
||||||
|
userMenuCfg.displayName = 1;
|
||||||
|
userMenuCfg.displayChangeName = 1;
|
||||||
|
}
|
||||||
|
Cryptpad.createUserAdminMenu(userMenuCfg);
|
||||||
|
|
||||||
|
var $userButton = toolbar.$userNameButton = $userAdmin.find('a.' + USERBUTTON_CLS);
|
||||||
|
$userButton.click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
Cryptpad.getLastName(function (err, lastName) {
|
||||||
|
if (err) { return void console.error("Cannot get last name", err); }
|
||||||
|
Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) {
|
||||||
|
if (newName === null && typeof(lastName) === "string") { return; }
|
||||||
|
if (newName === null) { newName = ''; }
|
||||||
|
Cryptpad.changeDisplayName(newName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Cryptpad.onDisplayNameChanged(function () {
|
||||||
|
Cryptpad.findCancelButton().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
return $userAdmin;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Events
|
||||||
|
var initClickEvents = function (toolbar, config) {
|
||||||
|
var removeDropdowns = function () {
|
||||||
|
toolbar.$toolbar.find('.cryptpad-dropdown').hide();
|
||||||
|
};
|
||||||
|
var cancelEditTitle = function (e) {
|
||||||
|
// Now we want to apply the title even if we click somewhere else
|
||||||
|
if ($(e.target).parents('.' + TITLE_CLS).length || !toolbar.title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var $title = toolbar.title;
|
||||||
|
if (!$title.find('input').is(':visible')) { return; }
|
||||||
|
|
||||||
|
// Press enter
|
||||||
|
var ev = $.Event("keyup");
|
||||||
|
ev.which = 13;
|
||||||
|
$title.find('input').trigger(ev);
|
||||||
|
};
|
||||||
|
// Click in the main window
|
||||||
|
var w = config.ifrw || window;
|
||||||
|
$(w).on('click', removeDropdowns);
|
||||||
|
$(w).on('click', cancelEditTitle);
|
||||||
|
// Click in iframes
|
||||||
|
try {
|
||||||
|
if (w.$ && w.$('iframe').length) {
|
||||||
|
config.ifrw.$('iframe').each(function (i, el) {
|
||||||
|
$(el.contentWindow).on('click', removeDropdowns);
|
||||||
|
$(el.contentWindow).on('click', cancelEditTitle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// empty try catch in case this iframe is problematic
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
var initNotifications = function (toolbar, config) {
|
||||||
|
// Display notifications when users are joining/leaving the session
|
||||||
|
var oldUserData;
|
||||||
|
if (!config.userList || !config.userList.list || !config.userList.userNetfluxId) { return; }
|
||||||
|
var userList = config.userList.list;
|
||||||
|
var userNetfluxId = config.userList.userNetfluxId;
|
||||||
|
if (typeof Cryptpad !== "undefined" && userList) {
|
||||||
|
var notify = function(type, name, oldname) {
|
||||||
|
// type : 1 (+1 user), 0 (rename existing user), -1 (-1 user)
|
||||||
|
if (typeof name === "undefined") { return; }
|
||||||
|
name = name || Messages.anonymous;
|
||||||
|
switch(type) {
|
||||||
|
case 1:
|
||||||
|
Cryptpad.log(Messages._getKey("notifyJoined", [name]));
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
oldname = (oldname === "") ? Messages.anonymous : oldname;
|
||||||
|
Cryptpad.log(Messages._getKey("notifyRenamed", [oldname, name]));
|
||||||
|
break;
|
||||||
|
case -1:
|
||||||
|
Cryptpad.log(Messages._getKey("notifyLeft", [name]));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Invalid type of notification");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var userPresent = function (id, user, data) {
|
||||||
|
if (!(user && user.uid)) {
|
||||||
|
console.log('no uid');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
console.log('no data');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
Object.keys(data).forEach(function (k) {
|
||||||
|
if (data[k] && data[k].uid === user.uid) { count++; }
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
userList.change.push(function (newdata) {
|
||||||
|
// Notify for disconnected users
|
||||||
|
if (typeof oldUserData !== "undefined") {
|
||||||
|
for (var u in oldUserData) {
|
||||||
|
// if a user's uid is still present after having left, don't notify
|
||||||
|
if (userList.users.indexOf(u) === -1) {
|
||||||
|
var temp = JSON.parse(JSON.stringify(oldUserData[u]));
|
||||||
|
delete oldUserData[u];
|
||||||
|
if (userPresent(u, temp, newdata || oldUserData) < 1) {
|
||||||
|
notify(-1, temp.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update the "oldUserData" object and notify for new users and names changed
|
||||||
|
if (typeof newdata === "undefined") { return; }
|
||||||
|
if (typeof oldUserData === "undefined") {
|
||||||
|
oldUserData = JSON.parse(JSON.stringify(newdata));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.readOnly === 0 && !oldUserData[userNetfluxId]) {
|
||||||
|
oldUserData = JSON.parse(JSON.stringify(newdata));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var k in newdata) {
|
||||||
|
if (k !== userNetfluxId && userList.users.indexOf(k) !== -1) {
|
||||||
|
if (typeof oldUserData[k] === "undefined") {
|
||||||
|
// if the same uid is already present in the userdata, don't notify
|
||||||
|
if (!userPresent(k, newdata[k], oldUserData)) {
|
||||||
|
notify(1, newdata[k].name);
|
||||||
|
}
|
||||||
|
} else if (oldUserData[k].name !== newdata[k].name) {
|
||||||
|
notify(0, newdata[k].name, oldUserData[k].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldUserData = JSON.parse(JSON.stringify(newdata));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Main
|
||||||
|
|
||||||
|
Bar.create = function (cfg) {
|
||||||
|
var config = cfg || {};
|
||||||
|
Cryptpad = config.common;
|
||||||
|
Messages = Cryptpad.Messages;
|
||||||
|
config.readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1;
|
||||||
|
config.displayed = config.displayed || [];
|
||||||
|
config.network = cfg.network || Cryptpad.getNetwork();
|
||||||
|
|
||||||
|
var toolbar = {};
|
||||||
|
|
||||||
|
toolbar.connected = false;
|
||||||
|
toolbar.firstConnection = true;
|
||||||
|
|
||||||
|
var $toolbar = toolbar.$toolbar = createRealtimeToolbar(config);
|
||||||
|
toolbar.$leftside = $toolbar.find('.'+Bar.constants.leftside);
|
||||||
|
toolbar.$rightside = $toolbar.find('.'+Bar.constants.rightside);
|
||||||
|
toolbar.$top = $toolbar.find('.'+Bar.constants.top);
|
||||||
|
toolbar.$history = $toolbar.find('.'+Bar.constants.history);
|
||||||
|
|
||||||
|
toolbar.$userAdmin = $toolbar.find('.'+Bar.constants.userAdmin);
|
||||||
|
|
||||||
|
// Create the subelements
|
||||||
|
var tb = {};
|
||||||
|
tb['userlist'] = createUserList;
|
||||||
|
tb['share'] = createShare;
|
||||||
|
tb['fileshare'] = createFileShare;
|
||||||
|
tb['title'] = createTitle;
|
||||||
|
tb['lag'] = createLag;
|
||||||
|
tb['spinner'] = createSpinner;
|
||||||
|
tb['state'] = createState;
|
||||||
|
tb['limit'] = createLimit;
|
||||||
|
tb['newpad'] = createNewPad;
|
||||||
|
tb['useradmin'] = createUserAdmin;
|
||||||
|
|
||||||
|
|
||||||
|
var addElement = toolbar.addElement = function (arr, additionnalCfg, init) {
|
||||||
|
if (typeof additionnalCfg === "object") { $.extend(true, config, additionnalCfg); }
|
||||||
|
arr.forEach(function (el) {
|
||||||
|
if (typeof el !== "string" || !el.trim()) { return; }
|
||||||
|
if (typeof tb[el] === "function") {
|
||||||
|
if (!init && config.displayed.indexOf(el) !== -1) { return; } // Already done
|
||||||
|
toolbar[el] = tb[el](toolbar, config);
|
||||||
|
if (!init) { config.displayed.push(el); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addElement(config.displayed, {}, true);
|
||||||
|
initUserList(toolbar, config);
|
||||||
|
|
||||||
|
toolbar['linkToMain'] = createLinkToMain(toolbar, config);
|
||||||
|
|
||||||
|
if (!config.realtime) { toolbar.connected = true; }
|
||||||
|
|
||||||
|
initClickEvents(toolbar, config);
|
||||||
|
initNotifications(toolbar, config);
|
||||||
|
|
||||||
|
var failed = toolbar.failed = function () {
|
||||||
|
toolbar.connected = false;
|
||||||
|
if (toolbar.state) {
|
||||||
|
toolbar.state.text(Messages.disconnected);
|
||||||
|
}
|
||||||
|
checkLag(toolbar, config);
|
||||||
|
};
|
||||||
|
toolbar.reconnecting = function (userId) {
|
||||||
|
if (config.userList) { config.userList.userNetfluxId = userId; }
|
||||||
|
toolbar.connected = false;
|
||||||
|
if (toolbar.state) {
|
||||||
|
toolbar.state.text(Messages.reconnecting);
|
||||||
|
}
|
||||||
|
checkLag(toolbar, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
// On log out, remove permanently the realtime elements of the toolbar
|
||||||
|
Cryptpad.onLogout(function () {
|
||||||
|
failed();
|
||||||
|
if (toolbar.useradmin) { toolbar.useradmin.hide(); }
|
||||||
|
if (toolbar.userlist) { toolbar.userlist.hide(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
return toolbar;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Bar;
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@ -1,77 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style>
|
|
||||||
html, body{
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
textarea{
|
|
||||||
position: absolute;
|
|
||||||
top: 5vh;
|
|
||||||
left: 0px;
|
|
||||||
border: 0px;
|
|
||||||
|
|
||||||
padding-top: 15px;
|
|
||||||
width: 100%;
|
|
||||||
height: 95vh;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100vh;
|
|
||||||
|
|
||||||
font-size: 30px;
|
|
||||||
background-color: #073642;
|
|
||||||
color: #839496;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
/* disallow textarea resizes */
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea[disabled] {
|
|
||||||
background-color: #275662;
|
|
||||||
color: #637476;
|
|
||||||
}
|
|
||||||
|
|
||||||
#panel {
|
|
||||||
position: fixed;
|
|
||||||
top: 0px;
|
|
||||||
right: 0px;
|
|
||||||
width: 100%;
|
|
||||||
height: 5vh;
|
|
||||||
z-index: 95;
|
|
||||||
background-color: #777;
|
|
||||||
/* min-height: 75px; */
|
|
||||||
}
|
|
||||||
#run {
|
|
||||||
display: block;
|
|
||||||
float: right;
|
|
||||||
height: 100%;
|
|
||||||
width: 10vw;
|
|
||||||
z-index: 100;
|
|
||||||
line-height: 5vw;
|
|
||||||
font-size: 1.5em;
|
|
||||||
background-color: #222;
|
|
||||||
color: #CCC;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 5%;
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<textarea></textarea>
|
|
||||||
<div id="panel">
|
|
||||||
<!-- TODO update this element when new users join -->
|
|
||||||
<span id="users"></span>
|
|
||||||
<!-- what else should go in the panel? -->
|
|
||||||
<a href="#" id="run">RUN</a>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,162 +0,0 @@
|
|||||||
define([
|
|
||||||
'/api/config?cb=' + Math.random().toString(16).substring(2),
|
|
||||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
|
||||||
'/bower_components/chainpad-crypto/crypto.js',
|
|
||||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
|
||||||
'/common/cryptpad-common.js',
|
|
||||||
'/bower_components/jquery/dist/jquery.min.js'
|
|
||||||
], function (Config, Realtime, Crypto, TextPatcher, Cryptpad) {
|
|
||||||
var $ = window.jQuery;
|
|
||||||
|
|
||||||
var secret = Cryptpad.getSecrets();
|
|
||||||
|
|
||||||
var $textarea = $('textarea'),
|
|
||||||
$run = $('#run');
|
|
||||||
|
|
||||||
var module = {};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
initialState: '',
|
|
||||||
websocketURL: Config.websocketURL,
|
|
||||||
channel: secret.channel,
|
|
||||||
crypto: Crypto.createEncryptor(secret.key),
|
|
||||||
};
|
|
||||||
var initializing = true;
|
|
||||||
|
|
||||||
var setEditable = function (bool) { $textarea.attr('disabled', !bool); };
|
|
||||||
var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
|
|
||||||
|
|
||||||
setEditable(false);
|
|
||||||
|
|
||||||
var onInit = config.onInit = function (info) {
|
|
||||||
window.location.hash = info.channel + secret.key;
|
|
||||||
$(window).on('hashchange', function() { window.location.reload(); });
|
|
||||||
};
|
|
||||||
|
|
||||||
var onRemote = config.onRemote = function (info) {
|
|
||||||
if (initializing) { return; }
|
|
||||||
|
|
||||||
var userDoc = info.realtime.getUserDoc();
|
|
||||||
var current = canonicalize($textarea.val());
|
|
||||||
|
|
||||||
var op = TextPatcher.diff(current, userDoc);
|
|
||||||
|
|
||||||
var elem = $textarea[0];
|
|
||||||
|
|
||||||
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
|
|
||||||
return TextPatcher.transformCursor(elem[attr], op);
|
|
||||||
});
|
|
||||||
|
|
||||||
$textarea.val(userDoc);
|
|
||||||
elem.selectionStart = selects[0];
|
|
||||||
elem.selectionEnd = selects[1];
|
|
||||||
|
|
||||||
// TODO do something on external messages
|
|
||||||
// http://webdesign.tutsplus.com/tutorials/how-to-display-update-notifications-in-the-browser-tab--cms-23458
|
|
||||||
};
|
|
||||||
|
|
||||||
var onReady = config.onReady = function (info) {
|
|
||||||
module.patchText = TextPatcher.create({
|
|
||||||
realtime: info.realtime
|
|
||||||
// logging: true
|
|
||||||
});
|
|
||||||
initializing = false;
|
|
||||||
setEditable(true);
|
|
||||||
$textarea.val(info.realtime.getUserDoc());
|
|
||||||
};
|
|
||||||
|
|
||||||
var onAbort = config.onAbort = function (info) {
|
|
||||||
setEditable(false);
|
|
||||||
window.alert("Server Connection Lost");
|
|
||||||
};
|
|
||||||
|
|
||||||
var onLocal = config.onLocal = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
module.patchText(canonicalize($textarea.val()));
|
|
||||||
};
|
|
||||||
|
|
||||||
var rt = window.rt = Realtime.start(config);
|
|
||||||
|
|
||||||
var splice = function (str, index, chars) {
|
|
||||||
var count = chars.length;
|
|
||||||
return str.slice(0, index) + chars + str.slice((index -1) + count);
|
|
||||||
};
|
|
||||||
|
|
||||||
var setSelectionRange = function (input, start, end) {
|
|
||||||
if (input.setSelectionRange) {
|
|
||||||
input.focus();
|
|
||||||
input.setSelectionRange(start, end);
|
|
||||||
} else if (input.createTextRange) {
|
|
||||||
var range = input.createTextRange();
|
|
||||||
range.collapse(true);
|
|
||||||
range.moveEnd('character', end);
|
|
||||||
range.moveStart('character', start);
|
|
||||||
range.select();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var setCursor = function (el, pos) {
|
|
||||||
setSelectionRange(el, pos, pos);
|
|
||||||
};
|
|
||||||
|
|
||||||
var state = {};
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
$textarea.on('keydown', function (e) {
|
|
||||||
// track when control keys are pushed down
|
|
||||||
//switch (e.key) { }
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
$textarea.on('keyup', function (e) {
|
|
||||||
// track when control keys are released
|
|
||||||
});
|
|
||||||
|
|
||||||
//$textarea.on('change', onLocal);
|
|
||||||
$textarea.on('keypress', function (e) {
|
|
||||||
onLocal();
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Tab':
|
|
||||||
// insert a tab wherever the cursor is...
|
|
||||||
var start = $textarea.prop('selectionStart');
|
|
||||||
var end = $textarea.prop('selectionEnd');
|
|
||||||
if (typeof start !== 'undefined') {
|
|
||||||
if (start === end) {
|
|
||||||
$textarea.val(function (i, val) {
|
|
||||||
return splice(val, start, "\t");
|
|
||||||
});
|
|
||||||
setCursor($textarea[0], start +1);
|
|
||||||
} else {
|
|
||||||
// indentation?? this ought to be fun.
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// simulate a keypress so the event goes through..
|
|
||||||
// prevent default behaviour for tab
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
onLocal();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput']
|
|
||||||
.forEach(function (evt) {
|
|
||||||
$textarea.on(evt, onLocal);
|
|
||||||
});
|
|
||||||
|
|
||||||
$run.click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var content = $textarea.val();
|
|
||||||
|
|
||||||
try {
|
|
||||||
eval(content); // jshint ignore:line
|
|
||||||
} catch (err) {
|
|
||||||
// FIXME don't use alert, make an errorbox
|
|
||||||
window.alert(err.message);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue