diff --git a/css/jgrowl.css b/css/jgrowl.css
index abbe3253..9b1ebc25 100644
--- a/css/jgrowl.css
+++ b/css/jgrowl.css
@@ -2,7 +2,6 @@
div.jGrowl {
z-index: 1000;
color: #fff;
- font-size: 12px;
}
/** Special IE6 Style Positioning **/
@@ -83,7 +82,7 @@ div.jGrowl div.jGrowl-notification, div.jGrowl div.jGrowl-closer {
background-color: #777;
zoom: 1;
width: 235px;
- padding: 10px;
+ padding: 15px 20px;
margin-top: 5px;
margin-bottom: 5px;
text-align: left;
@@ -98,7 +97,7 @@ div.jGrowl div.jGrowl-notification {
div.jGrowl div.jGrowl-notification,
div.jGrowl div.jGrowl-closer {
- margin: 10px;
+ margin: 20px;
}
div.jGrowl div.jGrowl-notification div.jGrowl-header {
diff --git a/css/main.css b/css/main.css
index 1d2dff21..b7f80c89 100644
--- a/css/main.css
+++ b/css/main.css
@@ -58,6 +58,11 @@ div, span, a, ul, li, textarea, input {
background-color: #777;
}
+input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] {
+ cursor: not-allowed;
+ background-color: #f5f5f5;
+}
+
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active,
@@ -102,6 +107,18 @@ hr {
margin: 30px 0;
}
+#file-title i {
+ margin: 3px 5px 0;
+}
+
+.dropdown-menu {
+ border-color: #ddd
+}
+
+.dropdown-menu i {
+ margin-right: 5px;
+}
+
#navbar {
position: static;
}
@@ -233,13 +250,3 @@ hr {
border-right: 5px solid #525252;
border-left: 0;
}
-
-#message-container {
- position: absolute;
- right: 20px;
- bottom: 20px;
- width: 200px;
- background-color: #eee;
- margin: 0;
- font-weight: bold;
-}
\ No newline at end of file
diff --git a/index.html b/index.html
index 9bba4e80..5de1d179 100644
--- a/index.html
+++ b/index.html
@@ -10,6 +10,7 @@
+
@@ -48,12 +49,12 @@
+
+
+
+
+
The file ""
+ is synchronized with these locations:
+
+
+
+
NOTE: Removing a
+ synchronized location will not delete any file.
+
The file ""
+ is not synchronized.
+
+
NOTE: You can add
+ synchronized locations using the top-right menu.
+
+
+
+
diff --git a/js/base64.js b/js/base64.js
new file mode 100644
index 00000000..5baba0d8
--- /dev/null
+++ b/js/base64.js
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2010 Nick Galbreath
+ * http://code.google.com/p/stringencoders/source/browse/#svn/trunk/javascript
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/* base64 encode/decode compatible with window.btoa/atob
+ *
+ * window.atob/btoa is a Firefox extension to convert binary data (the "b")
+ * to base64 (ascii, the "a").
+ *
+ * It is also found in Safari and Chrome. It is not available in IE.
+ *
+ * if (!window.btoa) window.btoa = base64.encode
+ * if (!window.atob) window.atob = base64.decode
+ *
+ * The original spec's for atob/btoa are a bit lacking
+ * https://developer.mozilla.org/en/DOM/window.atob
+ * https://developer.mozilla.org/en/DOM/window.btoa
+ *
+ * window.btoa and base64.encode takes a string where charCodeAt is [0,255]
+ * If any character is not [0,255], then an DOMException(5) is thrown.
+ *
+ * window.atob and base64.decode take a base64-encoded string
+ * If the input length is not a multiple of 4, or contains invalid characters
+ * then an DOMException(5) is thrown.
+ */
+var base64 = {};
+base64.PADCHAR = '=';
+base64.ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+base64.makeDOMException = function() {
+ // sadly in FF,Safari,Chrome you can't make a DOMException
+ var e, tmp;
+
+ try {
+ return new DOMException(DOMException.INVALID_CHARACTER_ERR);
+ } catch (tmp) {
+ // not available, just passback a duck-typed equiv
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error/prototype
+ var ex = new Error("DOM Exception 5");
+
+ // ex.number and ex.description is IE-specific.
+ ex.code = ex.number = 5;
+ ex.name = ex.description = "INVALID_CHARACTER_ERR";
+
+ // Safari/Chrome output format
+ ex.toString = function() { return 'Error: ' + ex.name + ': ' + ex.message; };
+ return ex;
+ }
+}
+
+base64.getbyte64 = function(s,i) {
+ // This is oddly fast, except on Chrome/V8.
+ // Minimal or no improvement in performance by using a
+ // object with properties mapping chars to value (eg. 'A': 0)
+ var idx = base64.ALPHA.indexOf(s.charAt(i));
+ if (idx === -1) {
+ throw base64.makeDOMException();
+ }
+ return idx;
+}
+
+base64.decode = function(s) {
+ // convert to string
+ s = '' + s;
+ var getbyte64 = base64.getbyte64;
+ var pads, i, b10;
+ var imax = s.length
+ if (imax === 0) {
+ return s;
+ }
+
+ if (imax % 4 !== 0) {
+ throw base64.makeDOMException();
+ }
+
+ pads = 0
+ if (s.charAt(imax - 1) === base64.PADCHAR) {
+ pads = 1;
+ if (s.charAt(imax - 2) === base64.PADCHAR) {
+ pads = 2;
+ }
+ // either way, we want to ignore this last block
+ imax -= 4;
+ }
+
+ var x = [];
+ for (i = 0; i < imax; i += 4) {
+ b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) |
+ (getbyte64(s,i+2) << 6) | getbyte64(s,i+3);
+ x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff));
+ }
+
+ switch (pads) {
+ case 1:
+ b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6);
+ x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff));
+ break;
+ case 2:
+ b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12);
+ x.push(String.fromCharCode(b10 >> 16));
+ break;
+ }
+ return x.join('');
+}
+
+base64.getbyte = function(s,i) {
+ var x = s.charCodeAt(i);
+ if (x > 255) {
+ throw base64.makeDOMException();
+ }
+ return x;
+}
+
+base64.encode = function(s) {
+ if (arguments.length !== 1) {
+ throw new SyntaxError("Not enough arguments");
+ }
+ var padchar = base64.PADCHAR;
+ var alpha = base64.ALPHA;
+ var getbyte = base64.getbyte;
+
+ var i, b10;
+ var x = [];
+
+ // convert to string
+ s = '' + s;
+
+ var imax = s.length - s.length % 3;
+
+ if (s.length === 0) {
+ return s;
+ }
+ for (i = 0; i < imax; i += 3) {
+ b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2);
+ x.push(alpha.charAt(b10 >> 18));
+ x.push(alpha.charAt((b10 >> 12) & 0x3F));
+ x.push(alpha.charAt((b10 >> 6) & 0x3f));
+ x.push(alpha.charAt(b10 & 0x3f));
+ }
+ switch (s.length - imax) {
+ case 1:
+ b10 = getbyte(s,i) << 16;
+ x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
+ padchar + padchar);
+ break;
+ case 2:
+ b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8);
+ x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
+ alpha.charAt((b10 >> 6) & 0x3f) + padchar);
+ break;
+ }
+ return x.join('');
+}
diff --git a/js/gdrive.js b/js/gdrive.js
index 7736ec3f..74659c72 100644
--- a/js/gdrive.js
+++ b/js/gdrive.js
@@ -125,7 +125,7 @@ var gdrive = (function($) {
method = 'PUT';
}
- var base64Data = btoa(content);
+ var base64Data = base64.encode(content);
var multipartRequestBody = delimiter
+ 'Content-Type: application/json\r\n\r\n'
+ JSON.stringify(metadata) + delimiter + 'Content-Type: '
diff --git a/js/main.js b/js/main.js
index 5daf412b..4bfd9497 100644
--- a/js/main.js
+++ b/js/main.js
@@ -6,6 +6,7 @@ function showError(msg) {
function showMessage(msg, iconClass, options) {
options = options || {};
+ iconClass = iconClass || "icon-info-sign";
$.jGrowl("
" + msg, options);
}
@@ -36,20 +37,6 @@ function onOnline() {
}
function autoClean() {
- $("#message-container .msg-temp").each(function() {
- var displayTime = $(this).data("displayTime");
- if (displayTime + 5000 < currentTime) {
- $(this).animate({ opacity : 'hide' }, 600, function() {
- $(this).remove();
- if ($("#message-container div").length === 0) {
- $("#message-container").addClass("hide");
- }
- });
- }
- });
- if ($("#message-container div").length === 0) {
- $("#message-container").addClass("hide");
- }
// Try to reconnect if we are offline but we have some network
if (offline === true && navigator.onLine === true
&& offlineTime + 60000 < currentTime) {
@@ -174,6 +161,7 @@ var fileManager = (function($) {
fileManager.deleteFile();
fileManager.selectFile();
});
+ $(".action-refresh-manage-sync").click(refreshManageSync);
$("#file-title").click(function() {
$(this).hide();
$("#file-title-input").show().focus();
@@ -181,53 +169,35 @@ var fileManager = (function($) {
$("#file-title-input").blur(function() {
var title = $.trim($(this).val());
if (title) {
- var fileIndex = localStorage["file.current"];
- localStorage[fileIndex + ".title"] = title;
+ var fileIndexTitle = localStorage["file.current"] + ".title";
+ if (title != localStorage[fileIndexTitle]) {
+ localStorage[fileIndexTitle] = title;
+ updateFileDescList();
+ updateFileTitleUI();
+ save = true;
+ }
}
$(this).hide();
$("#file-title").show();
- fileManager.updateFileDescList();
- fileManager.updateFileTitleUI();
- save = true;
});
$(".action-download-md").click(
function() {
var content = $("#wmd-input").val();
- var uriContent = "data:application/octet-stream,"
- + encodeURIComponent(content);
+ var uriContent = "data:application/octet-stream;base64,"
+ + base64.encode(content);
window.open(uriContent, 'file');
});
$(".action-download-html").click(
function() {
var content = $("#wmd-preview").html();
- var uriContent = "data:application/octet-stream,"
- + encodeURIComponent(content);
+ var uriContent = "data:application/octet-stream;base64,"
+ + base64.encode(content);
window.open(uriContent, 'file');
});
- $(".action-upload-gdrive")
- .click(
- function() {
- $(".file-sync-indicator").removeClass("hide");
- var fileIndex = localStorage["file.current"];
- var content = localStorage[fileIndex + ".content"];
- var title = localStorage[fileIndex + ".title"];
- (function(fileIndex) {
- gdrive
- .createFile(
- title,
- content,
- function(fileSyncIndex) {
- if (fileSyncIndex) {
- localStorage[fileIndex + ".sync"] += fileSyncIndex
- + ";";
- } else {
- showError("Error while creating file on Google Drive");
- }
- });
- })(fileIndex);
- });
+ $(".action-upload-gdrive").click(uploadGdrive);
};
+ var fileDescList = [];
fileManager.selectFile = function() {
// If file system does not exist
if (!localStorage["file.counter"] || !localStorage["file.list"]) {
@@ -235,15 +205,15 @@ var fileManager = (function($) {
localStorage["file.counter"] = 0;
localStorage["file.list"] = ";";
}
- this.updateFileDescList();
+ updateFileDescList();
// If no file create one
- if (this.fileDescList.length === 0) {
+ if (fileDescList.length === 0) {
this.createFile();
- this.updateFileDescList();
+ updateFileDescList();
}
// If no default file take first one
if (!localStorage["file.current"]) {
- localStorage["file.current"] = this.fileDescList[0].index;
+ localStorage["file.current"] = fileDescList[0].index;
}
// Update the editor and the file title
var fileIndex = localStorage["file.current"];
@@ -251,7 +221,7 @@ var fileManager = (function($) {
core.createEditor(function() {
save = true;
});
- this.updateFileTitleUI();
+ updateFileTitleUI();
};
fileManager.createFile = function(title) {
@@ -280,36 +250,83 @@ var fileManager = (function($) {
localStorage.removeItem(fileIndex + ".content");
};
- fileManager.updateFileDescList = function() {
- this.fileDescList = [];
+ fileManager.saveFile = function() {
+ if (save) {
+ var content = $("#wmd-input").val();
+ var fileIndex = localStorage["file.current"];
+ localStorage[fileIndex + ".content"] = content;
+ synchronizer.addFile(fileIndex);
+ save = false;
+ }
+ };
+
+ // Remove a synchronization location associated to the file
+ fileManager.removeSync = function(sync) {
+ var fileIndexSync = localStorage["file.current"] + ".sync";
+ localStorage[fileIndexSync] = localStorage[fileIndexSync].replace(";"
+ + sync + ";", ";");
+
+ localStorage.removeItem(fileIndexSync + ".etag");
+ };
+
+ function uploadGdrive() {
+ $(".file-sync-indicator").removeClass("hide");
+ var fileIndex = localStorage["file.current"];
+ var content = localStorage[fileIndex + ".content"];
+ var title = localStorage[fileIndex + ".title"];
+ gdrive.createFile(title, content, function(fileSyncIndex) {
+ if (fileSyncIndex) {
+ localStorage[fileIndex + ".sync"] += fileSyncIndex + ";";
+ updateFileTitleUI();
+ showMessage('The file "' + title
+ + '" will now be synchronized on Google Drive.');
+ } else {
+ showError("Error while creating file on Google Drive.");
+ }
+ });
+ }
+
+ function updateFileDescList() {
+ fileDescList = [];
$("#file-selector").empty();
var fileIndexList = localStorage["file.list"].split(";");
for ( var i = 1; i < fileIndexList.length - 1; i++) {
var fileIndex = fileIndexList[i];
var title = localStorage[fileIndex + ".title"];
- this.fileDescList.push({ "index" : fileIndex, "title" : title });
+ fileDescList.push({ index : fileIndex, title : title });
}
- this.fileDescList.sort(function(a, b) {
+ fileDescList.sort(function(a, b) {
if (a.title.toLowerCase() < b.title.toLowerCase())
return -1;
if (a.title.toLowerCase() > b.title.toLowerCase())
return 1;
return 0;
});
- };
+ }
+ ;
+
+ function updateFileTitleUI() {
+ function composeTitle(fileIndex) {
+ var result = localStorage[fileIndex + ".title"];
+ var sync = localStorage[fileIndex + ".sync"];
+ if (sync.indexOf(SYNC_PROVIDER_GDRIVE) !== -1) {
+ result = '
' + result;
+ }
+ return result;
+ }
- fileManager.updateFileTitleUI = function() {
// Update the editor and the file title
var fileIndex = localStorage["file.current"];
var title = localStorage[fileIndex + ".title"];
document.title = "StackEdit - " + title;
+ $("#file-title").html(composeTitle(fileIndex));
$(".file-title").text(title);
$("#file-title-input").val(title);
$("#file-selector").empty();
- for ( var i = 0; i < this.fileDescList.length; i++) {
- var fileDesc = this.fileDescList[i];
- var a = $("
").text(fileDesc.title);
+ for ( var i = 0; i < fileDescList.length; i++) {
+ var fileDesc = fileDescList[i];
+ var a = $("").html(composeTitle(fileDesc.index));
var li = $("").append(a);
if (fileDesc.index == fileIndex) {
li.addClass("disabled");
@@ -323,17 +340,39 @@ var fileManager = (function($) {
}
$("#file-selector").append(li);
}
- };
+ }
+ ;
- fileManager.saveFile = function() {
- if (save) {
- var content = $("#wmd-input").val();
- var fileIndex = localStorage["file.current"];
- localStorage[fileIndex + ".content"] = content;
- synchronizer.addFile(fileIndex);
- save = false;
+ function refreshManageSync() {
+ var fileIndex = localStorage["file.current"];
+ var fileSyncIndexList = localStorage[fileIndex + ".sync"].split(";");
+ $(".msg-no-sync, .msg-sync-list").addClass("hide");
+ $("#manage-sync-list .input-append").remove();
+ if (fileSyncIndexList.length > 2) {
+ $(".msg-sync-list").removeClass("hide");
+ } else {
+ $(".msg-no-sync").removeClass("hide");
}
- };
+ for ( var i = 1; i < fileSyncIndexList.length - 1; i++) {
+ var sync = fileSyncIndexList[i];
+ (function(sync) {
+ var line = $("