$(function(){ var editor , converter , autoInterval , githubUser , paperImgPath = '/img/notebook_paper_200x200.gif' , profile = { theme: '/static/js/ace/theme/idle_fingers' , showPaper: false , currentMd: '' , autosave: { enabled: true , interval: 3000 // might be too aggressive; don't want to block UI for large saves. } , wordcount: true , current_filename : $("#page-name").val() , dropbox: { filepath: '/Dillinger/' } } // Feature detect ish var dillinger = 'dillinger' , dillingerElem = document.createElement(dillinger) , dillingerStyle = dillingerElem.style , domPrefixes = 'Webkit Moz O ms Khtml'.split(' ') // Cache some shit var $theme = $('#theme-list') , $preview = $('#preview') , $autosave = $('#autosave') , $wordcount = $('#wordcount') , $import_github = $('#import_github') , $wordcounter = $('#wordcounter') , $filename = $('#filename'), $pagename = $("#page-name") // Hash of themes and their respective background colors var bgColors = { 'chrome': '#bbbbbb' , 'clouds': '#7AC9E3' , 'clouds_midnight': '#5F9EA0' , 'cobalt': '#4d586b' , 'crimson_editor': '#ffffff' , 'dawn': '#DADCAD' , 'eclipse': '#6C7B8A' , 'idle_fingers': '#DEB887' , 'kr_theme': '#434343' , 'merbivore': '#3E353E' , 'merbivore_soft': '#565156' , 'mono_industrial': '#C0C0C0' , 'monokai': '#F5DEB3' , 'pastel_on_dark': '#676565' , 'solarized-dark': '#0E4B5A' , 'solarized_light': '#dfcb96' , 'textmate': '#fff' , 'tomorrow': '#0e9211' , 'tomorrow_night': '#333536' , 'tomorrow_night_blue': '#3a4150' , 'tomorrow_night_bright': '#3A3A3A' , 'tomorrow_night_eighties': '#474646' , 'twilight': '#534746' , 'vibrant_ink': '#363636' } /// UTILS ================= /** * Utility method to async load a JavaScript file. * * @param {String} The name of the file to load * @param {Function} Optional callback to be executed after the script loads. * @return {void} */ function asyncLoad(filename,cb){ (function(d,t){ var leScript = d.createElement(t) , scripts = d.getElementsByTagName(t)[0] leScript.async = 1 leScript.src = filename scripts.parentNode.insertBefore(leScript,scripts) leScript.onload = function(){ cb && cb() } }(document,'script')) } /** * Utility method to determin if localStorage is supported or not. * * @return {Boolean} */ function hasLocalStorage(){ // http://mathiasbynens.be/notes/localstorage-pattern var storage try{ if(localStorage.getItem) {storage = localStorage} }catch(e){} return storage } /** * Grab the user's profile from localStorage and stash in "profile" variable. * * @return {Void} */ function getUserProfile(){ var p; try{ p = JSON.parse( localStorage.profile ); // Need to merge in any undefined/new properties from last release // Meaning, if we add new features they may not have them in profile p = $.extend(true, profile, p) }catch(e){ p = profile } if (p.filename != $pagename.val()) { updateUserProfile({ filename: $pagename.val(), currentMd: "" }); } profile = p } /** * Update user's profile in localStorage by merging in current profile with passed in param. * * @param {Object} An object containg proper keys and values to be JSON.stringify'd * @return {Void} */ function updateUserProfile(obj){ localStorage.clear() localStorage.profile = JSON.stringify( $.extend(true, profile, obj) ) } /** * Utility method to test if particular property is supported by the browser or not. * Completely ripped from Modernizr with some mods. * Thx, Modernizr team! * * @param {String} The property to test * @return {Boolean} */ function prefixed(prop){ return testPropsAll(prop, 'pfx') } /** * A generic CSS / DOM property test; if a browser supports * a certain property, it won't return undefined for it. * A supported CSS property returns empty string when its not yet set. * * @param {Object} A hash of properties to test * @param {String} A prefix * @return {Boolean} */ function testProps( props, prefixed ) { for ( var i in props ) { if( dillingerStyle[ props[i] ] !== undefined ) { return prefixed === 'pfx' ? props[i] : true } } return false } /** * Tests a list of DOM properties we want to check against. * We specify literally ALL possible (known and/or likely) properties on * the element including the non-vendor prefixed one, for forward- * compatibility. * * @param {String} The name of the property * @param {String} The prefix string * @return {Boolean} */ function testPropsAll( prop, prefixed ) { var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1) , props = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' ') return testProps(props, prefixed) } /** * Normalize the transitionEnd event across browsers. * * @return {String} */ function normalizeTransitionEnd() { var transEndEventNames = { 'WebkitTransition' : 'webkitTransitionEnd' , 'MozTransition' : 'transitionend' , 'OTransition' : 'oTransitionEnd' , 'msTransition' : 'msTransitionEnd' // maybe? , 'transition' : 'transitionend' } return transEndEventNames[ prefixed('transition') ] } /** * Generate a random filename. * * @param {String} The file type's extension * @return {String} */ function generateRandomFilename(ext){ return 'dillinger_' +(new Date()).toISOString().replace(/[\.:-]/g, "_")+ '.' + ext } /** * Get current filename from contenteditable field. * * @return {String} */ function getCurrentFilenameFromField(){ return $('#filename > span[contenteditable="true"]').text() } /** * Set current filename from profile. * * @param {String} Optional string to force set the value. * @return {String} */ function setCurrentFilenameField(str){ $('#filename > span[contenteditable="true"]').text( str || profile.current_filename || "Untitled Document") } /** * Returns the full text of an element and all its children. * The script recursively traverses all text nodes, and returns a * concatenated string of all texts. * * Taken from * http://stackoverflow.com/questions/2653670/innertext-textcontent-vs-retrieving-each-text-node * * @param node * @return {int} */ function getTextInElement(node) { if (node.nodeType === 3) { return node.data; } var txt = ''; if (node = node.firstChild) do { txt += getTextInElement(node); } while (node = node.nextSibling); return txt; } /** * Counts the words in a string * * @param string * @return int */ function countWords(string) { var words = string.replace(/W+/g, ' ').match(/\S+/g); return words && words.length || 0; } /** * Initialize application. * * @return {Void} */ function init(){ if( !hasLocalStorage() ) { sadPanda() } else{ // Attach to jQuery support object for later use. $.support.transitionEnd = normalizeTransitionEnd(); getUserProfile(); initAce(); initUi(); marked.setOptions({ gfm: true, tables: true, pedantic: false, sanitize: false, smartLists: true, smartypants: false, langPrefix: 'lang-' }); converter = marked; bindPreview(); bindNav(); bindKeyboard(); bindDelegation(); bindFilenameField(); bindWordCountEvents(); autoSave(); initWordCount(); refreshWordCount(); } } /** * Initialize theme and other options of Ace editor. * * @return {Void} */ function initAce(){ editor = ace.edit("editor"); } // end initAce /** * Initialize various UI elements based on userprofile data. * * @return {Void} */ function initUi(){ // Set proper theme value in theme dropdown fetchTheme(profile.theme, function(){ $theme.find('li > a[data-value="'+profile.theme+'"]').addClass('selected') editor.getSession().setUseWrapMode(true); editor.setShowPrintMargin(false); editor.getSession().setMode('ace/mode/markdown'); editor.getSession().setValue( profile.currentMd || editor.getSession().getValue()) // Immediately populate the preview
previewMd(); }); // Set text for dis/enable autosave / word counter $autosave.html( profile.autosave.enabled ? ' Disable Autosave' : ' Enable Autosave' ) $wordcount.html( !profile.wordcount ? ' Disabled Word Count' : ' Enabled Word Count' ) // Check for logged in Github user and notifiy githubUser = $import_github.attr('data-github-username'); githubUser && Notifier.showMessage("What's Up " + githubUser, 1000); setCurrentFilenameField(); /* BEGIN RE-ARCH STUFF */ $('.dropdown-toggle').dropdown(); /* END RE-ARCH STUFF */ } /// HANDLERS ================= /** * Clear the markdown and text and the subsequent HTML preview. * * @return {Void} */ function clearSelection(){ editor.getSession().setValue(""); previewMd(); } // TODO: WEBSOCKET MESSAGE? /** * Save the markdown via localStorage - isManual is from a click or key event. * * @param {Boolean} * @return {Void} */ function saveFile(isManual){ updateUserProfile({currentMd: editor.getSession().getValue()}) isManual && Notifier.showMessage(Notifier.messages.docSavedLocal); if (isManual) { updateUserProfile({ currentMd: "" }); var data = { name: $("#page-name").val(), message: $("#page-message").val(), content: editor.getSession().getValue() }; $.post(window.location, data, function(){ location.href = "/" + data['name']; }); } } /** * Enable autosave for a specific interval. * * @return {Void} */ function autoSave(){ if(profile.autosave.enabled){ autoInterval = setInterval( function(){ // firefox barfs if I don't pass in anon func to setTimeout. saveFile(); }, profile.autosave.interval); } else{ clearInterval( autoInterval ) } } /** * Clear out user profile data in localStorage. * * @return {Void} */ function resetProfile(){ // For some reason, clear() is not working in Chrome. localStorage.clear(); // Let's turn off autosave profile.autosave.enabled = false // Delete the property altogether --> need ; for JSHint bug. ; delete localStorage.profile; // Now reload the page to start fresh window.location.reload(); // Notifier.showMessage(Notifier.messages.profileCleared, 1400) } /** * Dropbown nav handler to update the current theme. * * @return {Void} */ function changeTheme(e){ // check for same theme var $target = $(e.target) if( $target.attr('data-value') === profile.theme) { return } else{ // add/remove class $theme.find('li > a.selected').removeClass('selected') $target.addClass('selected') // grabnew theme var newTheme = $target.attr('data-value') $(e.target).blur() fetchTheme(newTheme, function(){ Notifier.showMessage(Notifier.messages.profileUpdated) }) } } // TODO: Maybe we just load them all once and stash in appcache? /** * Dynamically appends a script tag with the proper theme and then applies that theme. * * @param {String} The theme name * @param {Function} Optional callback * @return {Void} */ function fetchTheme(th, cb){ var name = th.split('/').pop() asyncLoad("/static/js/ace/theme-"+ name +".js", function(){ editor.setTheme(th) cb && cb() updateBg(name) updateUserProfile({theme: th}) }) // end asyncLoad } // end fetchTheme(t) /** * Change the body background color based on theme. * * @param {String} The theme name * @return {Void} */ function updateBg(name){ // document.body.style.backgroundColor = bgColors[name] } /** * Clientside update showing rendered HTML of Markdown. * * @return {Void} */ function previewMd(){ var unmd = editor.getSession().getValue() , md = converter(unmd) $preview .html('') // unnecessary? .html(md) refreshWordCount(); } function refreshWordCount(selectionCount){ var msg = "Words: "; if (selectionCount !== undefined) { msg += selectionCount + " of "; } if(profile.wordcount){ $wordcounter.text(msg + countWords(getTextInElement($preview[0]))); } } /** * Stash current file name in the user's profile. * * @param {String} Optional string to force the value * @return {Void} */ function updateFilename(str){ // Check for string because it may be keyup event object var f if(typeof str === 'string'){ f = str }else { f = getCurrentFilenameFromField() } updateUserProfile( {current_filename: f }) } /** * XHR Post Markdown to get a md file. Appends response to hidden iframe to * automatically download the file. * * @return {Void} */ function fetchMarkdownFile(){ // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME var unmd = editor.getSession().getValue() function _doneHandler(a, b, response){ a = b = null // JSHint complains if a, b are null in method var resp = JSON.parse(response.responseText) // console.dir(resp) document.getElementById('downloader').src = '/files/md/' + resp.data } function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var mdConfig = { type: 'POST', data: "unmd=" + encodeURIComponent(unmd), dataType: 'json', url: '/factory/fetch_markdown', error: _failHandler, success: _doneHandler } $.ajax(mdConfig) } /** * XHR Post Markdown to get a html file. Appends response to hidden iframe to * automatically download the file. * * @return {Void} */ function fetchHtmlFile(){ // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME var unmd = editor.getSession().getValue() function _doneHandler(jqXHR, data, response){ // console.dir(resp) var resp = JSON.parse(response.responseText) document.getElementById('downloader').src = '/files/html/' + resp.data } function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'POST', data: "unmd=" + encodeURIComponent(unmd), dataType: 'json', url: '/factory/fetch_html', error: _failHandler, success: _doneHandler } $.ajax(config) } function fetchPdfFile(){ var unmd = editor.getSession().getValue() function _doneHandler(jqXHR, data, response){ var resp = JSON.parse(response.responseText) document.getElementById('downloader').src = '/files/pdf/' + resp.data } function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'POST', data: "unmd=" + encodeURIComponent(unmd), dataType: 'json', url: '/factory/fetch_pdf', error: _failHandler, success: _doneHandler } $.ajax(config) } function showHtml(){ // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME var unmd = editor.getSession().getValue() function _doneHandler(jqXHR, data, response){ // console.dir(resp) var resp = JSON.parse(response.responseText) $('#myModalBody').text(resp.data) $('#myModal').modal() } function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'POST', data: "unmd=" + encodeURIComponent(unmd), dataType: 'json', url: '/factory/fetch_html_direct', error: _failHandler, success: _doneHandler } $.ajax(config) } /** * Show a sad panda because they are using a shitty browser. * * @return {Void} */ function sadPanda(){ // TODO: ACTUALLY SHOW A SAD PANDA. alert('Sad Panda - No localStorage for you!') } /** * Show the modal for the "About Dillinger" information. * * @return {Void} */ function showAboutInfo(){ $('.modal-header h3').text("What's the deal with Dillinger?") // TODO: PULL THIS OUT AND RENDER VIA TEMPLATE FROM XHR OR STASH IN PAGE FOR SEO AND CLONE var aboutContent = "

Dillinger is an online cloud-enabled, HTML5, buzzword-filled Markdown editor.

" + "

Dillinger was designed and developed by @joemccann because he needed a decent Markdown editor.

" + "

Dillinger is a 100% open source project so fork the code and contribute!

" + "

Follow Dillinger on Twitter at @dillingerapp

" + "

Follow Joe McCann on Twitter at @joemccann

" $('.modal-body').html(aboutContent) $('#modal-generic').modal({ keyboard: true, backdrop: true, show: true }) } /** * Show the modal for the "Preferences". * * @return {Void} */ function showPreferences(){ $('.modal-header h3').text("Preferences") // TODO: PULL THIS OUT AND RENDER VIA TEMPLATE FROM XHR OR STASH IN PAGE FOR SEO AND CLONE var prefContent = '
' +'' +'
' $('.modal-body').html(prefContent) $('#modal-generic').modal({ keyboard: true, backdrop: true, show: true }) } /// UI RELATED ================= /** * Toggles the paper background image. * * @return {Void} */ function togglePaper(){ $preview.css('backgroundImage', !profile.showPaper ? 'url("'+paperImgPath+'")' : 'url("")' ) updateUserProfile({showPaper: !profile.showPaper}) Notifier.showMessage(Notifier.messages.profileUpdated) } /** * Toggles the autosave feature. * * @return {Void} */ function toggleAutoSave(){ $autosave.html( profile.autosave.enabled ? ' Disable Autosave' : ' Enable Autosave' ) updateUserProfile({autosave: {enabled: !profile.autosave.enabled }}) autoSave() } function initWordCount(){ if (profile.wordcount) { $wordcounter.removeClass('hidden'); // Modify the width of the document name $filename.addClass('show-word-count-filename-adjust') } else { $wordcounter.addClass('hidden'); // Modify the width of the document name $filename.removeClass('show-word-count-filename-adjust') } } /** * Toggles the wordcounter feature. * * @return {Void} */ function toggleWordCount() { $wordcount.html( profile.wordcount ? ' Disabled Word Count' : ' Enabled Word Count' ) updateUserProfile({wordcount: !profile.wordcount }) initWordCount(); } /** * Bind keyup handler to the editor. * * @return {Void} */ function bindFilenameField(){ $('#filename > span[contenteditable="true"]').bind('keyup', updateFilename) } /** * Makes the selection check fire after every mouse up event. * * @return void */ function bindWordCountEvents() { $preview.bind('mouseup', checkForSelection); } /** * Checks if there is some selected text. If so, the word counter gets updated. * * @return void */ function checkForSelection() { if (profile.wordcount) { var selection = window.getSelection().toString(); if (selection !== "") { refreshWordCount(countWords(selection)); } else { refreshWordCount(); } } } /** * Bind keyup handler to the editor. * * @return {Void} */ function bindPreview(){ $('#editor').bind('keyup', previewMd); } /** * Bind navigation elements. * * @return {Void} */ function bindNav(){ $theme .find('li > a') .bind('click', function(e){ changeTheme(e) return false }) $('#clear') .on('click', function(){ clearSelection() return false }) $("#save_dropbox") .on('click', function(){ profile.current_filename = profile.current_filename || '/Dillinger/' + generateRandomFilename('md') Dropbox.putMarkdownFile() saveFile() return false }) $("#save_googledrive") .on('click', function() { //profile.current_filename = profile.current_filename || generateRandomFilename('md') GoogleDrive.save() saveFile() }) $("#save-native").on('click', function() { saveFile(true); }); $(".modal-body").delegate("#paper", "click", function(){ togglePaper() return false }) $("#autosave") .on('click', function(){ toggleAutoSave() return false }) $("#wordcount") .on('click', function(){ toggleWordCount() return false }) $('#reset') .on('click', function(){ resetProfile() return false }) $import_github .on('click', function(){ Github.fetchRepos() return false }) $('#import_dropbox') .on('click', function(){ Dropbox.searchDropbox() return false }) $('#import_googledrive') .on('click', function(){ GoogleDrive.search() return false }) $('#export_md') .on('click', function(){ fetchMarkdownFile() $('.dropdown').removeClass('open') return false }) $('#export_html') .on('click', function(){ fetchHtmlFile() $('.dropdown').removeClass('open') return false }) $('#export_pdf') .on('click', function(){ fetchPdfFile() $('.dropdown').removeClass('open') return false }) $('#show_html') .on('click', function(){ showHtml() $('.dropdown').removeClass('open') return false }) $('#preferences'). on('click', function(){ showPreferences() return false }) $('#about'). on('click', function(){ showAboutInfo() return false }) $('#cheat'). on('click', function(){ window.open("https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet", "_blank") return false }) } // end bindNav() /** * Bind special keyboard handlers. * * @return {Void} */ function bindKeyboard(){ // CMD+s TO SAVE DOC key('command+s, ctrl+s', function(e){ saveFile(true) e.preventDefault() // so we don't save the webpage - native browser functionality }) var saveCommand = { name: "save", bindKey: { mac: "Command-S", win: "Ctrl-S" }, exec: function(){ saveFile(true) } } var fileForUrlNamer = { name: "filenamer", bindKey: { mac: "Command-Shift-M", win: "Ctrl-Shift-M" }, exec: function(){ var profile = JSON.parse(localStorage.profile); alert( profile.current_filename.replace(/\s/g, '-').toLowerCase()) } } editor.commands.addCommand(saveCommand) editor.commands.addCommand(fileForUrlNamer) } /** * Bind dynamically added elements' handlers. * * @return {Void} */ function bindDelegation(){ $(document) .on('click', '.repo', function(){ var repoName = $(this).parent('li').attr('data-repo-name') Github.isRepoPrivate = $(this).parent('li').attr('data-repo-private') === 'true' ? true : false Github.fetchBranches( repoName ) return false }) .on('click', '.branch', function(){ var repo = $(this).parent('li').attr('data-repo-name') , sha = $(this).parent('li').attr('data-commit-sha') Github.currentBranch = $(this).text() Github.fetchTreeFiles( repo, sha ) return false }) .on('click', '.tree_file', function(){ var file = $(this).parent('li').attr('data-tree-file') Github.fetchMarkdownFile(file) return false }) .on('click', '.dropbox_file', function(){ // We stash the current filename in the local profile only; not in localStorage. // Upon success of fetching, we add it to localStorage. var dboxFilePath = $(this).parent('li').attr('data-file-path') profile.current_filename = dboxFilePath.split('/').pop().replace('.md', '') Dropbox.setFilePath( dboxFilePath ) Dropbox.fetchMarkdownFile( dboxFilePath ) return false }) .on('click', '.googledrive_file', function(){ var fileId = $(this).parent('li').attr('data-file-id') profile.current_filename = $(this).html() GoogleDrive.fileId = fileId GoogleDrive.get() return false }) // Check for support of drag/drop if('draggable' in document.createElement('span')){ $('#editor') .on('dragover', function (e) { e.preventDefault() e.stopPropagation() }) .on('drop', function(e) { e.preventDefault() e.stopPropagation() // fetch FileList object var originalEvent = e.originalEvent , files = originalEvent.target.files || originalEvent.dataTransfer.files , reader = new FileReader() , i = 0 , file , name // find the first text file do { file = files[i++] } while (file && file.type.substr(0, 4) !== 'text' && file.name.substr(file.name.length - 3) !== '.md') if (!file) return reader.onload = function (lE) { editor.getSession().setValue(lE.target.result) previewMd() } reader.readAsText(file) }) } // end if draggable } // end bindDelegation() /// MODULES ================= // Notification Module var Notifier = (function(){ var _el = $('#notify') return { messages: { profileUpdated: "Profile updated" , profileCleared: "Profile cleared" , docSavedLocal: "Document saved locally" , docSavedServer: "Document saved on our server" , docSavedDropbox: "Document saved on dropbox" , dropboxImportNeeded: "Please import a file from dropbox first." }, showMessage: function(msg,delay){ // TODO: FIX ANIMATION QUEUE PROBLEM - .stop() doesn't work. _el .text('') .stop() .text(msg) .slideDown(250, function(){ _el .delay(delay || 1000) .slideUp(250) }) } // end showMesssage } // end return obj })() // end IIFE // Github API Module var Github = (function(){ // Sorting regardless of upper/lowercase function _alphaNumSort(m,n) { var a = m.url.toLowerCase() var b = n.url.toLowerCase() if (a === b) { return 0 } if (isNaN(m) || isNaN(n)){ return ( a > b ? 1 : -1)} else {return m-n} } // Test for md file extension function _isMdFile(file){ return (/(\.md)|(\.markdown)/i).test(file) } // Returns an array of only md files from a tree function _extractMdFiles(repoName, treefiles){ /* mode: "100644" path: ".gitignore" sha: "7a1aeb2497018aeb0c44e220d4b84f2d245e3033" size: 110 type: "blob" url: "https://api.github.com/repos/joemccann/express/git/blobs/7a1aeb2497018aeb0c44e220d4b84f2d245e3033" */ // https://raw.github.com/joemccann/express/master/History.md var sorted = [] , raw = 'https://raw.github.com' , slash = '/' treefiles.forEach(function(el){ if( _isMdFile(el.path) ){ var fullpath if( Github.isRepoPrivate){ fullpath = el.url } else{ // we go straight to raw as it's faster (don't need to base64 decode the sha as in the private case) fullpath = raw + slash + githubUser + slash + repoName + slash + Github.currentBranch + slash + el.path } var item = { link: fullpath , path: el.path , sha: el.sha } sorted.push( item ) } }) // end forEach() return sorted } // Show a list of repos function _listRepos(repos){ var list = '' $('.modal-header h3').text('Your Github Repos') $('.modal-body').html(list) $('#modal-generic').modal({ keyboard: true, backdrop: true, show: true }) return false } // Show a list of branches function _listBranches(repoName, branches){ var list = '' branches.forEach(function(item){ var name = item.name , commit = item.commit.sha list += '
  • ' + name + '
  • ' }) $('.modal-header h3').text(repoName) $('.modal-body') .find('ul') .find('li') .remove() .end() .append(list) } // Show a list of tree files function _listTreeFiles(repoName, treefiles){ var mdFiles = _extractMdFiles(repoName, treefiles) , list = '' mdFiles.forEach(function(item){ // add class to
  • if private list += Github.isRepoPrivate ? '
  • ' + item.path + '
  • ' : '
  • ' + item.path + '
  • ' }) $('.modal-header h3').text(repoName) $('.modal-body') .find('ul') .find('li') .remove() .end() .append(list) } return{ currentBranch: '', isRepoPrivate: false, fetchRepos: function(){ function _beforeSendHandler(){ Notifier.showMessage('Fetching Repos...') } function _doneHandler(a, b, response){ a = b = null response = JSON.parse(response.responseText) // console.dir(response) if( !response.length ) { Notifier.showMessage('No repos available!') } else { _listRepos(response) } // end else } // end done handler function _failHandler(resp,err){ alert(resp.responseText || "Roh-roh. Something went wrong. :(") } var config = { type: 'POST', dataType: 'text', url: '/import/github/repos', beforeSend: _beforeSendHandler, error: _failHandler, success: _doneHandler } $.ajax(config) }, // end fetchRepos fetchBranches: function(repoName){ function _beforeSendHandler(){ Notifier.showMessage('Fetching Branches for Repo '+repoName) } function _doneHandler(a, b, response){ a = b = null response = JSON.parse(response.responseText) //console.dir(response) if( !response.length ) { Notifier.showMessage('No branches available!') $('#modal-generic').modal('hide') } else { _listBranches(repoName, response) } // end else } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'POST', dataType: 'json', data: 'repo=' + repoName, url: '/import/github/branches', beforeSend: _beforeSendHandler, error: _failHandler, success: _doneHandler } $.ajax(config) }, // end fetchBranches() fetchTreeFiles: function(repoName, sha){ function _beforeSendHandler(){ Notifier.showMessage('Fetching Tree for Repo '+repoName) } function _doneHandler(a, b, response){ a = b = null response = JSON.parse(response.responseText) // console.log('\nFetch Tree Files...') // console.dir(response) if( !response.tree.length ) { Notifier.showMessage('No tree files available!') $('#modal-generic').modal('hide') } else { _listTreeFiles(repoName, response.tree) } // end else } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'POST', dataType: 'json', data: 'repo=' + repoName + '&sha=' + sha, url: '/import/github/tree_files', beforeSend: _beforeSendHandler, error: _failHandler, success: _doneHandler } $.ajax(config) }, // end fetchTreeFiles() fetchMarkdownFile: function(filename){ function _doneHandler(a, b, response){ a = b = null response = JSON.parse(response.responseText) // console.dir(response) if( response.error ) { Notifier.showMessage('No markdown for you!') $('#modal-generic').modal('hide') } else{ $('#modal-generic').modal('hide') editor.getSession().setValue( response.data ) // Update it in localStorage var name = filename.split('/').pop() updateFilename(name) // Show it in the field setCurrentFilenameField(name) previewMd() } // end else } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } function _alwaysHandler(){ $('.dropdown').removeClass('open') } var config = { type: 'POST', dataType: 'json', data: 'mdFile=' + filename, url: '/import/github/file', error: _failHandler, success: _doneHandler, complete: _alwaysHandler } $.ajax(config) } // end fetchMarkdownFile() } // end return obj })() // end IIFE var GoogleDrive = (function() { function _errorHandler(a, b, res) { Notifier.showMessage(res.responseText ); } function renderSearchResults(a, b, res) { var result = JSON.parse(res.responseText) , list = '' $('.modal-header h3').text('Your Google Drive Files') $('.modal-body').html(list) $('#modal-generic').modal({ keyboard: true, backdrop: true, show: true }) } function renderFile(a, b, res) { var result = JSON.parse(res.responseText); $('#modal-generic').modal('hide') editor.getSession().setValue(result.content) previewMd() } // TODO: what to do if access token expires? return { fileId: null, search: function() { $.ajax({ dataType: 'json', url: '/import/googledrive', beforeSend: function() { Notifier.showMessage('Searching for .md files') }, error: _errorHandler, success: renderSearchResults }); }, get: function() { $.ajax({ dataType: 'json', url: '/fetch/googledrive?fileId=' + this.fileId, error: _errorHandler, success: renderFile }); }, save: function() { var content = encodeURIComponent(editor.getSession().getValue()); var postData = 'title=' + encodeURIComponent(profile.current_filename)+ '.md' + '&content=' + content $.ajax({ dataType: 'json', type: 'post', data: postData, url: '/save/googledrive?fileId=' + (GoogleDrive.fileId || ''), error: _errorHandler, success: function(a, b, res) { var response = JSON.parse(res.responseText); if (response.id) { GoogleDrive.fileId = response.id Notifier.showMessage('Document saved on Google Drive') } else { Notifier.showMessage('An error occurred!') } } }); } } })(); // Dropbox Module var Dropbox = (function(){ // Sorting regardless of upper/lowercase // TODO: Let's be DRY and merge this with the // sort method in Github module. function _alphaNumSort(m,n) { var a = m.path.toLowerCase() var b = n.path.toLowerCase() if (a === b) { return 0 } if (isNaN(m) || isNaN(n)){ return ( a > b ? 1 : -1)} else {return m-n} } function _listMdFiles(files){ var list = '' $('.modal-header h3').text('Your Dropbox Files') $('.modal-body').html(list) $('#modal-generic').modal({ keyboard: true, backdrop: true, show: true }) return false } function _encodeFilename(path){ return encodeURIComponent( path.split('/').pop() ) } function _removeFilenameFromPath(path){ // capture the name var name = path.split('/').pop() // then just replace with nothing on the path. boom. return path.replace(name, '') } return { fetchAccountInfo: function(){ function _beforeSendHandler(){ Notifier.showMessage('Fetching User Info from Dropbox') } function _doneHandler(a, b, response){ var resp = JSON.parse(response.responseText) // console.log('\nFetch User Info...') // console.dir(resp) Notifier .showMessage('Sup '+ resp.display_name) } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'GET', dataType: 'json', url: '/account/dropbox', beforeSend: _beforeSendHandler, error: _failHandler, success: _doneHandler } $.ajax(config) }, // end fetchAccuntInfo() fetchMetadata: function(){ function _beforeSendHandler(){ Notifier.showMessage('Fetching Metadata') } function _doneHandler(a, b, response){ var resp = JSON.parse(response.responseText) window.console && window.console.log && console.dir(resp) } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var config = { type: 'GET', dataType: 'json', url: '/dropbox/metadata', beforeSend: _beforeSendHandler, error: _failHandler, success: _doneHandler } $.ajax(config) }, // end fetchMetadata() searchDropbox: function(){ function _beforeSendHandler(){ Notifier.showMessage('Searching for .md Files') } function _doneHandler(a, b, response){ a = b = null var resp = JSON.parse(response.responseText) if(resp.hasOwnProperty('statusCode') && resp.statusCode === 401){ // {"statusCode":401,"data":"{\"error\": \"Access token is disabled.\"}"} var respData = JSON.parse(resp.data) Notifier.showMessage('Error! ' + respData.error, 1000) return setTimeout(function(){ Notifier.showMessage('Reloading!') window.location.reload() }, 1250) } if(!resp.length){ Notifier.showMessage('No .md files found!') } else{ // console.dir(resp) _listMdFiles(resp) } } // end done handler function _failHandler(resp,err){ alert(resp.responseText || "Roh-roh. Something went wrong. :(") } var config = { type: 'GET', dataType: 'json', url: '/import/dropbox', beforeSend: _beforeSendHandler, error: _failHandler, success: _doneHandler } $.ajax(config) }, // end searchDropbox() fetchMarkdownFile: function(filename){ function _doneHandler(a, b, response){ response = JSON.parse(response.responseText) // console.dir(response) if( response.statusCode === 404 ) { var msg = JSON.parse( response.data ) Notifier.showMessage(msg.error) } else{ $('#modal-generic').modal('hide') // Update it in localStorage updateFilename(profile.current_filename) // Show it in the field setCurrentFilenameField() editor.getSession().setValue( response.data ) previewMd() } // end else } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } // Weird encoding mumbo jumbo columbo var enc = _encodeFilename(filename) var path = _removeFilenameFromPath(filename) filename = path + enc var config = { type: 'POST', dataType: 'json', data: 'mdFile=' + filename, url: '/fetch/dropbox', error: _failHandler, success: _doneHandler } $.ajax(config) }, // end fetchMarkdownFile() setFilePath: function(path){ path = _removeFilenameFromPath(path) updateUserProfile({dropbox: {filepath: path }}) }, putMarkdownFile: function(){ function _doneHandler(a, b, response){ a = b = null response = JSON.parse(response.responseText) // console.dir(response) if( response.statusCode >= 204 ) { var msg = JSON.parse( response.data ) Notifier.showMessage(msg.error, 5000) } else{ $('#modal-generic').modal('hide') // console.dir(JSON.parse(response.data)) Notifier.showMessage( Notifier.messages.docSavedDropbox ) } // end else } // end done handler function _failHandler(){ alert("Roh-roh. Something went wrong. :(") } var md = encodeURIComponent( editor.getSession().getValue() ) var postData = 'pathToMdFile=' + profile.dropbox.filepath + encodeURIComponent(profile.current_filename) + '.md' + '&fileContents=' + md var config = { type: 'POST', dataType: 'json', data: postData, url: '/save/dropbox', error: _failHandler, success: _doneHandler } $.ajax(config) } // end fetchMarkdownFile() } // end return obj })() // end IIFE init() // TODO: add window.resize() handlers. }) /** * Get scrollHeight of preview div * (code adapted from https://github.com/anru/rsted/blob/master/static/scripts/editor.js) * * @param {Object} The jQuery object for the preview div * @return {Int} The scrollHeight of the preview area (in pixels) */ function getScrollHeight($prevFrame) { // Different browsers attach the scrollHeight of a document to different // elements, so handle that here. if ($prevFrame[0].scrollHeight !== undefined) { return $prevFrame[0].scrollHeight; } else if ($prevFrame.find('html')[0].scrollHeight !== undefined && $prevFrame.find('html')[0].scrollHeight !== 0) { return $prevFrame.find('html')[0].scrollHeight; } else { return $prevFrame.find('body')[0].scrollHeight; } } /** * Scroll preview to match cursor position in editor session * (code adapted from https://github.com/anru/rsted/blob/master/static/scripts/editor.js) * * @return {Void} */ function syncPreview() { var $ed = window.ace.edit('editor'); var $prev = $('#preview'); var editorScrollRange = ($ed.getSession().getLength()); var previewScrollRange = (getScrollHeight($prev)); // Find how far along the editor is (0 means it is scrolled to the top, 1 // means it is at the bottom). var scrollFactor = $ed.getFirstVisibleRow() / editorScrollRange; // Set the scroll position of the preview pane to match. jQuery will // gracefully handle out-of-bounds values. $prev.scrollTop(scrollFactor * previewScrollRange); } window.onload = function(){ var $loading = $('#loading') if ($.support.transition){ $loading .bind( $.support.transitionEnd, function(){ $('#main').removeClass('bye') $loading.remove() }) .addClass('fade_slow'); } else { $('#main').removeClass('bye') $loading.remove() } /** * Bind synchronization of preview div to editor scroll and change * of editor cursor position. */ window.ace.edit('editor').session.on('changeScrollTop', syncPreview); window.ace.edit('editor').session.selection.on('changeCursor', syncPreview); }