CouchDB sync part 1
This commit is contained in:
parent
2be4919853
commit
8f5da7998f
@ -7,7 +7,7 @@
|
|||||||
"jquery": "2.0.3",
|
"jquery": "2.0.3",
|
||||||
"underscore": "1.5.1",
|
"underscore": "1.5.1",
|
||||||
"requirejs": "~2.1.11",
|
"requirejs": "~2.1.11",
|
||||||
"require-css": "0.1.2",
|
"require-css": "0.1.5",
|
||||||
"require-less": "0.1.2",
|
"require-less": "0.1.2",
|
||||||
"mousetrap": "~1.4.4",
|
"mousetrap": "~1.4.4",
|
||||||
"jgrowl": "~1.2.10",
|
"jgrowl": "~1.2.10",
|
||||||
|
144
couchdb/setup-db.js
Normal file
144
couchdb/setup-db.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
var request = require('request');
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
var validate = function(newDoc) {
|
||||||
|
Object.keys(newDoc).forEach(function(key) {
|
||||||
|
if(key[0] !== '_' && [
|
||||||
|
'updated',
|
||||||
|
'tags',
|
||||||
|
'title'
|
||||||
|
].indexOf(key) === -1) {
|
||||||
|
throw({forbidden: 'Unknown document attribute: ' + key});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var toString = Object.prototype.toString;
|
||||||
|
if(toString.call(newDoc._id) !== '[object String]') {
|
||||||
|
throw({forbidden: 'ID must be a string.'});
|
||||||
|
}
|
||||||
|
if(!newDoc._id.match(/[a-zA-Z0-9]{24}/)) {
|
||||||
|
throw({forbidden: 'Invalid ID format.'});
|
||||||
|
}
|
||||||
|
if(toString.call(newDoc.updated) !== '[object Number]') {
|
||||||
|
throw({forbidden: 'Update time must be an integer.'});
|
||||||
|
}
|
||||||
|
if(newDoc.updated > Date.now() + 60000) {
|
||||||
|
throw({forbidden: 'Update time is in the future, please check your clock!'});
|
||||||
|
}
|
||||||
|
if(toString.call(newDoc.title) !== '[object String]') {
|
||||||
|
throw({forbidden: 'Title must be a string.'});
|
||||||
|
}
|
||||||
|
if(!newDoc.title) {
|
||||||
|
throw({forbidden: 'Title is empty.'});
|
||||||
|
}
|
||||||
|
if(newDoc.title.length >= 256) {
|
||||||
|
throw({forbidden: 'Title too long.'});
|
||||||
|
}
|
||||||
|
if(newDoc.tags !== undefined) {
|
||||||
|
if(toString.call(newDoc.tags) !== '[object Array]') {
|
||||||
|
throw({forbidden: 'Tags must be an array.'});
|
||||||
|
}
|
||||||
|
if(newDoc.tags.length >= 16) {
|
||||||
|
throw({forbidden: 'Too many tags.'});
|
||||||
|
}
|
||||||
|
newDoc.tags.forEach(function(tag) {
|
||||||
|
if(toString.call(tag) !== '[object String]') {
|
||||||
|
throw({forbidden: 'Tags must contain strings only.'});
|
||||||
|
}
|
||||||
|
if(!tag) {
|
||||||
|
throw({forbidden: 'Tag is empty.'});
|
||||||
|
}
|
||||||
|
if(tag.length > 32) {
|
||||||
|
throw({forbidden: 'Tag is too long.'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var attachment = (newDoc._attachments || {}).content;
|
||||||
|
if(!attachment) {
|
||||||
|
throw({forbidden: 'Missing attached content.'});
|
||||||
|
}
|
||||||
|
if(attachment.content_type != 'text/plain') {
|
||||||
|
throw({forbidden: 'Invalid content type.'});
|
||||||
|
}
|
||||||
|
if(Object.keys(newDoc._attachments).length > 1) {
|
||||||
|
throw({forbidden: 'Too many attachments.'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var byUpdate = function(doc) {
|
||||||
|
emit(doc.updated, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
var byTagAndUpdate = function(doc) {
|
||||||
|
doc.tags && doc.tags.forEach(function(tag) {
|
||||||
|
emit([
|
||||||
|
tag,
|
||||||
|
doc.updated
|
||||||
|
], null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if(process.argv.length < 3) {
|
||||||
|
console.error('Missing URL parameter');
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = process.argv[2];
|
||||||
|
|
||||||
|
var ddocs = [
|
||||||
|
{
|
||||||
|
path: '/_design/validate',
|
||||||
|
body: {
|
||||||
|
validate_doc_update: validate.toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/_design/by_update',
|
||||||
|
body: {
|
||||||
|
views: {
|
||||||
|
default: {
|
||||||
|
map: byUpdate.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/_design/by_tag_and_update',
|
||||||
|
body: {
|
||||||
|
views: {
|
||||||
|
default: {
|
||||||
|
map: byTagAndUpdate.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
async.each(ddocs, function(ddoc, cb) {
|
||||||
|
|
||||||
|
request.get(url + ddoc.path, function(err, res) {
|
||||||
|
if(res && res.body) {
|
||||||
|
ddoc.body._rev = JSON.parse(res.body)._rev;
|
||||||
|
}
|
||||||
|
request.put({
|
||||||
|
url: url + ddoc.path,
|
||||||
|
json: true,
|
||||||
|
body: ddoc.body
|
||||||
|
}, function(err, res) {
|
||||||
|
if(err) {
|
||||||
|
return cb(res);
|
||||||
|
}
|
||||||
|
if(res.statusCode >= 300) {
|
||||||
|
return cb(res.body);
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.error(err);
|
||||||
|
} else {
|
||||||
|
console.log('All design documents updated successfully');
|
||||||
|
}
|
||||||
|
});
|
@ -314,7 +314,7 @@ define([
|
|||||||
// Create discussion index
|
// Create discussion index
|
||||||
var discussionIndex;
|
var discussionIndex;
|
||||||
do {
|
do {
|
||||||
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
|
discussionIndex = utils.id();
|
||||||
} while(_.has(newDiscussionList, discussionIndex));
|
} while(_.has(newDiscussionList, discussionIndex));
|
||||||
conflict.discussionIndex = discussionIndex;
|
conflict.discussionIndex = discussionIndex;
|
||||||
newDiscussionList[discussionIndex] = conflict;
|
newDiscussionList[discussionIndex] = conflict;
|
||||||
|
@ -27,6 +27,7 @@ define([], function() {
|
|||||||
constants.PICASA_IMPORT_IMG_URL = "/picasaImportImg";
|
constants.PICASA_IMPORT_IMG_URL = "/picasaImportImg";
|
||||||
constants.SSH_PUBLISH_URL = '/sshPublish';
|
constants.SSH_PUBLISH_URL = '/sshPublish';
|
||||||
constants.PDF_EXPORT_URL = "/pdfExport";
|
constants.PDF_EXPORT_URL = "/pdfExport";
|
||||||
|
constants.COUCHDB_URL = 'http://localhost:5984/documents';
|
||||||
|
|
||||||
// Site dependent
|
// Site dependent
|
||||||
constants.BASE_URL = "http://localhost/";
|
constants.BASE_URL = "http://localhost/";
|
||||||
|
@ -55,7 +55,7 @@ define([
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(windowId === undefined) {
|
if(windowId === undefined) {
|
||||||
windowId = utils.randomString();
|
windowId = utils.id();
|
||||||
storage.frontWindowId = windowId;
|
storage.frontWindowId = windowId;
|
||||||
}
|
}
|
||||||
var frontWindowId = storage.frontWindowId;
|
var frontWindowId = storage.frontWindowId;
|
||||||
|
@ -381,7 +381,7 @@ define([
|
|||||||
// Create discussion index
|
// Create discussion index
|
||||||
var discussionIndex;
|
var discussionIndex;
|
||||||
do {
|
do {
|
||||||
discussionIndex = utils.randomString();
|
discussionIndex = utils.id();
|
||||||
} while(_.has(discussionList, discussionIndex));
|
} while(_.has(discussionList, discussionIndex));
|
||||||
discussion.discussionIndex = discussionIndex;
|
discussion.discussionIndex = discussionIndex;
|
||||||
discussionList[discussionIndex] = discussion;
|
discussionList[discussionIndex] = discussion;
|
||||||
|
@ -7,7 +7,7 @@ define([
|
|||||||
"classes/Extension",
|
"classes/Extension",
|
||||||
"classes/FolderDescriptor",
|
"classes/FolderDescriptor",
|
||||||
"folderList",
|
"folderList",
|
||||||
"fileSystem",
|
"fileSystem"
|
||||||
], function($, _, constants, utils, storage, Extension, FolderDescriptor, folderList, fileSystem) {
|
], function($, _, constants, utils, storage, Extension, FolderDescriptor, folderList, fileSystem) {
|
||||||
|
|
||||||
var documentManager = new Extension("documentManager", 'Document Manager', false, true);
|
var documentManager = new Extension("documentManager", 'Document Manager', false, true);
|
||||||
@ -39,17 +39,17 @@ define([
|
|||||||
'<button class="btn btn-default button-delete" title="Delete"><i class="icon-trash"></i></button>',
|
'<button class="btn btn-default button-delete" title="Delete"><i class="icon-trash"></i></button>',
|
||||||
'<button class="btn btn-default button-rename" title="Rename"><i class="icon-pencil"></i></button>',
|
'<button class="btn btn-default button-rename" title="Rename"><i class="icon-pencil"></i></button>',
|
||||||
'<div class="name"><%= fileDesc.composeTitle() %></div>',
|
'<div class="name"><%= fileDesc.composeTitle() %></div>',
|
||||||
'<input type="text" class="input-rename form-control hide"></li>',
|
'<input type="text" class="input-rename form-control hide"></li>'
|
||||||
].join('');
|
].join('');
|
||||||
var selectFolderEltTmpl = [
|
var selectFolderEltTmpl = [
|
||||||
'<a href="#" class="list-group-item folder clearfix" data-folder-index="<%= folderDesc.folderIndex %>">',
|
'<a href="#" class="list-group-item folder clearfix" data-folder-index="<%= folderDesc.folderIndex %>">',
|
||||||
'<div class="pull-right file-count"><%= _.size(folderDesc.fileList) %></div>',
|
'<div class="pull-right file-count"><%= _.size(folderDesc.fileList) %></div>',
|
||||||
'<div class="name"><i class="icon-forward"></i> ',
|
'<div class="name"><i class="icon-forward"></i> ',
|
||||||
'<%= folderDesc.name %></div></a>',
|
'<%= folderDesc.name %></div></a>'
|
||||||
].join('');
|
].join('');
|
||||||
var selectedDocumentEltTmpl = [
|
var selectedDocumentEltTmpl = [
|
||||||
'<li class="list-group-item file clearfix">',
|
'<li class="list-group-item file clearfix">',
|
||||||
'<div class="name"><%= fileDesc.composeTitle() %></div></li>',
|
'<div class="name"><%= fileDesc.composeTitle() %></div></li>'
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
var isVisible;
|
var isVisible;
|
||||||
@ -86,7 +86,7 @@ define([
|
|||||||
return fileDesc.title.toLowerCase();
|
return fileDesc.title.toLowerCase();
|
||||||
}).reduce(function(result, fileDesc) {
|
}).reduce(function(result, fileDesc) {
|
||||||
return result + _.template(selectedDocumentEltTmpl, {
|
return result + _.template(selectedDocumentEltTmpl, {
|
||||||
fileDesc: fileDesc,
|
fileDesc: fileDesc
|
||||||
});
|
});
|
||||||
}, '').value();
|
}, '').value();
|
||||||
selectedDocumentListElt.innerHTML = '<ul class="file-list nav">' + selectedDocumentListHtml + '</ul>';
|
selectedDocumentListElt.innerHTML = '<ul class="file-list nav">' + selectedDocumentListHtml + '</ul>';
|
||||||
@ -147,7 +147,7 @@ define([
|
|||||||
_.size(orphanDocumentList),
|
_.size(orphanDocumentList),
|
||||||
'</div>',
|
'</div>',
|
||||||
'<div class="name"><i class="icon-folder"></i> ',
|
'<div class="name"><i class="icon-folder"></i> ',
|
||||||
'ROOT folder</div></a>',
|
'ROOT folder</div></a>'
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
// Add orphan documents
|
// Add orphan documents
|
||||||
@ -155,7 +155,7 @@ define([
|
|||||||
return fileDesc.title.toLowerCase();
|
return fileDesc.title.toLowerCase();
|
||||||
}).reduce(function(result, fileDesc) {
|
}).reduce(function(result, fileDesc) {
|
||||||
return result + _.template(documentEltTmpl, {
|
return result + _.template(documentEltTmpl, {
|
||||||
fileDesc: fileDesc,
|
fileDesc: fileDesc
|
||||||
});
|
});
|
||||||
}, '').value();
|
}, '').value();
|
||||||
orphanListHtml = orphanListHtml && '<ul class="nav">' + orphanListHtml + '</ul>';
|
orphanListHtml = orphanListHtml && '<ul class="nav">' + orphanListHtml + '</ul>';
|
||||||
@ -169,7 +169,7 @@ define([
|
|||||||
return fileDesc.title.toLowerCase();
|
return fileDesc.title.toLowerCase();
|
||||||
}).reduce(function(result, fileDesc) {
|
}).reduce(function(result, fileDesc) {
|
||||||
return result + _.template(documentEltTmpl, {
|
return result + _.template(documentEltTmpl, {
|
||||||
fileDesc: fileDesc,
|
fileDesc: fileDesc
|
||||||
});
|
});
|
||||||
}, '').value();
|
}, '').value();
|
||||||
fileListHtml = fileListHtml && '<ul class="nav">' + fileListHtml + '</ul>';
|
fileListHtml = fileListHtml && '<ul class="nav">' + fileListHtml + '</ul>';
|
||||||
@ -224,7 +224,7 @@ define([
|
|||||||
$(modalElt.querySelectorAll('.action-create-folder')).click(function() {
|
$(modalElt.querySelectorAll('.action-create-folder')).click(function() {
|
||||||
var folderIndex;
|
var folderIndex;
|
||||||
do {
|
do {
|
||||||
folderIndex = "folder." + utils.randomString();
|
folderIndex = "folder." + utils.id();
|
||||||
} while (_.has(folderList, folderIndex));
|
} while (_.has(folderList, folderIndex));
|
||||||
|
|
||||||
storage[folderIndex + ".name"] = constants.DEFAULT_FOLDER_NAME;
|
storage[folderIndex + ".name"] = constants.DEFAULT_FOLDER_NAME;
|
||||||
@ -286,13 +286,13 @@ define([
|
|||||||
_.size(orphanDocumentList),
|
_.size(orphanDocumentList),
|
||||||
'</div>',
|
'</div>',
|
||||||
'<div class="name"><i class="icon-forward"></i> ',
|
'<div class="name"><i class="icon-forward"></i> ',
|
||||||
'ROOT folder</div></a>',
|
'ROOT folder</div></a>'
|
||||||
].join('');
|
].join('');
|
||||||
selectFolderListHtml += _.chain(folderList).sortBy(function(folderDesc) {
|
selectFolderListHtml += _.chain(folderList).sortBy(function(folderDesc) {
|
||||||
return folderDesc.name.toLowerCase();
|
return folderDesc.name.toLowerCase();
|
||||||
}).reduce(function(result, folderDesc) {
|
}).reduce(function(result, folderDesc) {
|
||||||
return result + _.template(selectFolderEltTmpl, {
|
return result + _.template(selectFolderEltTmpl, {
|
||||||
folderDesc: folderDesc,
|
folderDesc: folderDesc
|
||||||
});
|
});
|
||||||
}, '').value();
|
}, '').value();
|
||||||
selectFolderListElt.innerHTML = selectFolderListHtml;
|
selectFolderListElt.innerHTML = selectFolderListHtml;
|
||||||
|
@ -68,7 +68,7 @@ define([
|
|||||||
var fileIndex = constants.TEMPORARY_FILE_INDEX;
|
var fileIndex = constants.TEMPORARY_FILE_INDEX;
|
||||||
if(!isTemporary) {
|
if(!isTemporary) {
|
||||||
do {
|
do {
|
||||||
fileIndex = "file." + utils.randomString();
|
fileIndex = "file." + utils.id();
|
||||||
} while(_.has(fileSystem, fileIndex));
|
} while(_.has(fileSystem, fileIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
248
public/res/helpers/couchdbHelper.js
Normal file
248
public/res/helpers/couchdbHelper.js
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
define([
|
||||||
|
"jquery",
|
||||||
|
"underscore",
|
||||||
|
"constants",
|
||||||
|
"core",
|
||||||
|
"utils",
|
||||||
|
"storage",
|
||||||
|
"logger",
|
||||||
|
"settings",
|
||||||
|
"eventMgr",
|
||||||
|
"classes/AsyncTask"
|
||||||
|
], function($, _, constants, core, utils, storage, logger, settings, eventMgr, AsyncTask) {
|
||||||
|
|
||||||
|
var couchdbHelper = {};
|
||||||
|
|
||||||
|
// Listen to offline status changes
|
||||||
|
var isOffline = false;
|
||||||
|
eventMgr.addListener("onOfflineChanged", function(isOfflineParam) {
|
||||||
|
isOffline = isOfflineParam;
|
||||||
|
});
|
||||||
|
|
||||||
|
couchdbHelper.uploadDocument = function(documentId, title, content, tags, rev, callback) {
|
||||||
|
var result;
|
||||||
|
var task = new AsyncTask();
|
||||||
|
task.onRun(function() {
|
||||||
|
if(tags) {
|
||||||
|
// Has to be an array
|
||||||
|
if(!_.isArray(tags)) {
|
||||||
|
tags = _.chain(('' + tags).split(/\s+/))
|
||||||
|
.compact()
|
||||||
|
.unique()
|
||||||
|
.value();
|
||||||
|
}
|
||||||
|
// Remove invalid tags
|
||||||
|
tags = tags.filter(function(tag) {
|
||||||
|
return _.isString(tag) && tag.length < 32;
|
||||||
|
});
|
||||||
|
// Limit the number of tags
|
||||||
|
tags = tags.slice(0, 16);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tags = undefined;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: constants.COUCHDB_URL,
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
_id: documentId || utils.id(),
|
||||||
|
title: title,
|
||||||
|
tags: tags,
|
||||||
|
updated: Date.now(),
|
||||||
|
_rev: rev,
|
||||||
|
_attachments: {
|
||||||
|
content: {
|
||||||
|
content_type: 'text\/plain',
|
||||||
|
data: utils.encodeBase64(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).done(function(data) {
|
||||||
|
result = data;
|
||||||
|
task.chain();
|
||||||
|
}).fail(function(jqXHR) {
|
||||||
|
handleError(jqXHR, task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
task.onSuccess(function() {
|
||||||
|
callback(undefined, result);
|
||||||
|
});
|
||||||
|
task.onError(function(error) {
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
task.enqueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbHelper.checkChanges = function(lastChangeId, syncLocations, callback) {
|
||||||
|
var changes;
|
||||||
|
var newChangeId = lastChangeId || 0;
|
||||||
|
var task = new AsyncTask();
|
||||||
|
task.onRun(function() {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: constants.COUCHDB_URL + '/_changes?' + $.param({
|
||||||
|
filter: '_doc_ids',
|
||||||
|
since: newChangeId,
|
||||||
|
include_docs: true,
|
||||||
|
attachments: true
|
||||||
|
}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
doc_ids: Object.keys(syncLocations)
|
||||||
|
})
|
||||||
|
}).done(function(data) {
|
||||||
|
newChangeId = data.last_seq;
|
||||||
|
changes = _.map(data.results, function(result) {
|
||||||
|
return result.deleted ? {
|
||||||
|
_id: result.id,
|
||||||
|
deleted: true
|
||||||
|
} : result.doc;
|
||||||
|
});
|
||||||
|
task.chain();
|
||||||
|
}).fail(function(jqXHR) {
|
||||||
|
handleError(jqXHR, task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
task.onSuccess(function() {
|
||||||
|
callback(undefined, changes, newChangeId);
|
||||||
|
});
|
||||||
|
task.onError(function(error) {
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
task.enqueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbHelper.downloadContent = function(documents, callback) {
|
||||||
|
var result = [];
|
||||||
|
var task = new AsyncTask();
|
||||||
|
task.onRun(function() {
|
||||||
|
function recursiveDownloadContent() {
|
||||||
|
if(documents.length === 0) {
|
||||||
|
return task.chain();
|
||||||
|
}
|
||||||
|
var document = documents[0];
|
||||||
|
result.push(document);
|
||||||
|
if(document.deleted || ((document._attachments || {}).content || {}).data !== undefined) {
|
||||||
|
documents.shift();
|
||||||
|
return task.chain(recursiveDownloadContent);
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: constants.COUCHDB_URL + '/' + encodeURIComponent(document._id),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
attachments: true
|
||||||
|
}
|
||||||
|
}).done(function(doc) {
|
||||||
|
documents.shift();
|
||||||
|
_.extend(document, doc);
|
||||||
|
task.chain(recursiveDownloadContent);
|
||||||
|
}).fail(function(jqXHR) {
|
||||||
|
handleError(jqXHR, task);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
task.chain(recursiveDownloadContent);
|
||||||
|
});
|
||||||
|
task.onSuccess(function() {
|
||||||
|
callback(undefined, result);
|
||||||
|
});
|
||||||
|
task.onError(function(error) {
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
task.enqueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbHelper.listDocuments = function(tag, updated, callback) {
|
||||||
|
var result;
|
||||||
|
var task = new AsyncTask();
|
||||||
|
task.onRun(function() {
|
||||||
|
var ddoc = '/_design/by_' + (tag ? 'tag_and_' : '') + 'update/_view/default';
|
||||||
|
var startKey = tag ? JSON.stringify([
|
||||||
|
tag,
|
||||||
|
updated || []
|
||||||
|
]) : updated;
|
||||||
|
var endKey = tag && JSON.stringify([
|
||||||
|
tag
|
||||||
|
]);
|
||||||
|
$.ajax({
|
||||||
|
url: constants.COUCHDB_URL + ddoc,
|
||||||
|
data: {
|
||||||
|
start_key: startKey,
|
||||||
|
end_key: endKey,
|
||||||
|
descending: true,
|
||||||
|
include_docs: true,
|
||||||
|
limit: 3,
|
||||||
|
reduce: false
|
||||||
|
},
|
||||||
|
dataType: 'json'
|
||||||
|
}).done(function(data) {
|
||||||
|
result = _.pluck(data.rows, 'doc');
|
||||||
|
task.chain();
|
||||||
|
}).fail(function(jqXHR) {
|
||||||
|
handleError(jqXHR, task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
task.onSuccess(function() {
|
||||||
|
callback(undefined, result);
|
||||||
|
});
|
||||||
|
task.onError(function(error) {
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
task.enqueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbHelper.deleteDocuments = function(docs) {
|
||||||
|
var task = new AsyncTask();
|
||||||
|
task.onRun(function() {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: constants.COUCHDB_URL + '/_bulk_docs',
|
||||||
|
data: JSON.stringify({
|
||||||
|
docs: docs.map(function(doc) {
|
||||||
|
return {
|
||||||
|
_id: doc._id,
|
||||||
|
_rev: doc._rev,
|
||||||
|
_deleted: true
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json'
|
||||||
|
}).done(function() {
|
||||||
|
task.chain();
|
||||||
|
}).fail(function(jqXHR) {
|
||||||
|
handleError(jqXHR, task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
task.enqueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleError(jqXHR, task) {
|
||||||
|
var error = {
|
||||||
|
code: jqXHR.status,
|
||||||
|
message: jqXHR.statusText,
|
||||||
|
reason: (jqXHR.responseJSON || {}).reason
|
||||||
|
};
|
||||||
|
var errorMsg;
|
||||||
|
if(error) {
|
||||||
|
logger.error(error);
|
||||||
|
// Try to analyze the error
|
||||||
|
if(typeof error === "string") {
|
||||||
|
errorMsg = error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errorMsg = "Error " + error.code + ": " + (error.reason || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.error(new Error(errorMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
return couchdbHelper;
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
<div class="layout-wrapper-l1">
|
<div class="layout-wrapper-l1">
|
||||||
<div class="layout-wrapper-l2">
|
<div class="layout-wrapper-l2">
|
||||||
<div class="navbar navbar-default ui-layout-north">
|
<div class="navbar navbar-default">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="nav left-space"></div>
|
<div class="nav left-space"></div>
|
||||||
<div class="nav right-space pull-right"></div>
|
<div class="nav right-space pull-right"></div>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-wrapper-l3">
|
<div class="layout-wrapper-l3">
|
||||||
<pre id="wmd-input" class="ui-layout-center form-control"><div class="editor-content" contenteditable=true></div><div class="editor-margin"></div></pre>
|
<pre id="wmd-input" class="form-control"><div class="editor-content" contenteditable=true></div><div class="editor-margin"></div></pre>
|
||||||
<div class="preview-panel">
|
<div class="preview-panel">
|
||||||
<div class="layout-resizer layout-resizer-preview"></div>
|
<div class="layout-resizer layout-resizer-preview"></div>
|
||||||
<div class="layout-toggler layout-toggler-navbar btn btn-info" title="Toggle navigation bar"><i class="icon-th"></i></div>
|
<div class="layout-toggler layout-toggler-navbar btn btn-info" title="Toggle navigation bar"><i class="icon-th"></i></div>
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<a href="#" data-toggle="collapse" data-target=".collapse-synchronize"
|
<a href="#" data-toggle="collapse" data-target=".collapse-synchronize"
|
||||||
class="list-group-item">
|
class="list-group-item">
|
||||||
<div><i class="icon-refresh"></i> Synchronize</div>
|
<div><i class="icon-refresh"></i> Synchronize</div>
|
||||||
<small>Backup, collaborate...</small>
|
<small>Open/save in the Cloud</small>
|
||||||
</a>
|
</a>
|
||||||
<div class="sub-menu collapse collapse-synchronize clearfix">
|
<div class="sub-menu collapse collapse-synchronize clearfix">
|
||||||
<ul class="nav alert alert-danger show-already-synchronized">
|
<ul class="nav alert alert-danger show-already-synchronized">
|
||||||
@ -101,6 +101,10 @@
|
|||||||
class="icon-refresh"></i> Manage synchronization</a></li>
|
class="icon-refresh"></i> Manage synchronization</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav">
|
<ul class="nav">
|
||||||
|
<li><a href="#" class="action-sync-import-dialog-couchdb"><i
|
||||||
|
class="icon-provider-couchdb"></i> Open from CouchDB <sup class="text-danger">beta</sup></a></li>
|
||||||
|
<li><a href="#" class="action-sync-export-dialog-couchdb"><i
|
||||||
|
class="icon-provider-couchdb"></i> Save on CouchDB <sup class="text-danger">beta</sup></a></li>
|
||||||
<li><a href="#" class="action-sync-import-dropbox"><i
|
<li><a href="#" class="action-sync-import-dropbox"><i
|
||||||
class="icon-provider-dropbox"></i> Open from Dropbox</a></li>
|
class="icon-provider-dropbox"></i> Open from Dropbox</a></li>
|
||||||
<li><a href="#" class="action-sync-export-dialog-dropbox"><i
|
<li><a href="#" class="action-sync-export-dialog-dropbox"><i
|
||||||
@ -544,6 +548,110 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal fade modal-download-couchdb">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal"
|
||||||
|
aria-hidden="true">×</button>
|
||||||
|
<h2 class="modal-title">Open from CouchDB</h2>
|
||||||
|
<br>
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="select-sync-import-couchdb-tag" class="col-sm-3 control-label">Filter by tag</label>
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<select id="select-sync-import-couchdb-tag" class="form-control">
|
||||||
|
<option value="">None</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<button class="btn btn-link"><i class="icon-tags"></i> Manage tags</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-horizontal hide">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="input-sync-import-couchdb-documentid" class="col-sm-3 control-label">Document
|
||||||
|
ID</label>
|
||||||
|
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input id="input-sync-import-couchdb-documentid" class="form-control"
|
||||||
|
placeholder="DocumentID">
|
||||||
|
<span class="help-block">Multiple IDs can be provided (space separated)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="document-list nav nav-pills">
|
||||||
|
<li class="pull-right dropdown"><a href="#"
|
||||||
|
data-toggle="dropdown"><i class="icon-check"></i> Selection
|
||||||
|
<b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="#" class="action-unselect-all"><i
|
||||||
|
class="icon-check-empty"></i> Unselect all</a></li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="#" class="action-delete-items"><i
|
||||||
|
class="icon-trash"></i> Delete</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="document-list">
|
||||||
|
</p>
|
||||||
|
<div class="list-group document-list"></div>
|
||||||
|
<div class="document-list">
|
||||||
|
<div class="please-wait">Please wait...</div>
|
||||||
|
<button class="more-documents btn btn-link">More documents!</button>
|
||||||
|
</div>
|
||||||
|
<p class="confirm-delete hide">The following documents will be
|
||||||
|
removed from the database:</p>
|
||||||
|
|
||||||
|
<div class="confirm-delete list-group selected-document-list hide"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#"
|
||||||
|
class="btn btn-default confirm-delete action-cancel hide">Cancel</a>
|
||||||
|
<a href="#"
|
||||||
|
class="btn btn-primary confirm-delete action-delete-items-confirm hide">Delete</a>
|
||||||
|
<a href="#" class="btn btn-default document-list" data-dismiss="modal">Cancel</a>
|
||||||
|
<a href="#" data-dismiss="modal"
|
||||||
|
class="btn btn-primary action-sync-import-couchdb document-list">Open</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal fade modal-upload-couchdb">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal"
|
||||||
|
aria-hidden="true">×</button>
|
||||||
|
<h2 class="modal-title">Save on CouchDB</h2>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
This will save "<span class="file-title"></span>" to CouchDB and keep it synchronized.
|
||||||
|
</p>
|
||||||
|
<blockquote>
|
||||||
|
<b>Tip:</b> You can use a
|
||||||
|
<a href="http://jekyllrb.com/docs/frontmatter/"
|
||||||
|
target="_blank">YAML front matter</a> to specify tags for your document.
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#" class="btn btn-default" data-dismiss="modal">Cancel</a>
|
||||||
|
<a href="#" data-dismiss="modal"
|
||||||
|
class="btn btn-primary action-sync-export-couchdb">OK</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="modal fade modal-manage-sync">
|
<div class="modal fade modal-manage-sync">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="layout-wrapper-l1">
|
<div class="layout-wrapper-l1">
|
||||||
<div class="layout-wrapper-l2">
|
<div class="layout-wrapper-l2">
|
||||||
<div class="navbar navbar-default ui-layout-north">
|
<div class="navbar navbar-default">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="nav left-space"></div>
|
<div class="nav left-space"></div>
|
||||||
<div class="nav right-space pull-right"></div>
|
<div class="nav right-space pull-right"></div>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-wrapper-l3">
|
<div class="layout-wrapper-l3">
|
||||||
<pre id="wmd-input" class="ui-layout-center form-control"><div class="editor-content"></div><div class="editor-margin"></div></pre>
|
<pre id="wmd-input" class="form-control"><div class="editor-content"></div><div class="editor-margin"></div></pre>
|
||||||
<div class="preview-panel">
|
<div class="preview-panel">
|
||||||
<div class="preview-container">
|
<div class="preview-container">
|
||||||
<div id="preview-contents">
|
<div id="preview-contents">
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
297
public/res/providers/couchdbProvider.js
Normal file
297
public/res/providers/couchdbProvider.js
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
define([
|
||||||
|
"jquery",
|
||||||
|
"underscore",
|
||||||
|
"constants",
|
||||||
|
"utils",
|
||||||
|
"storage",
|
||||||
|
"logger",
|
||||||
|
"classes/Provider",
|
||||||
|
"settings",
|
||||||
|
"eventMgr",
|
||||||
|
"fileMgr",
|
||||||
|
"fileSystem",
|
||||||
|
"editor",
|
||||||
|
"helpers/couchdbHelper"
|
||||||
|
], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, fileSystem, editor, couchdbHelper) {
|
||||||
|
|
||||||
|
var PROVIDER_COUCHDB = "couchdb";
|
||||||
|
|
||||||
|
var couchdbProvider = new Provider(PROVIDER_COUCHDB, "CouchDB");
|
||||||
|
|
||||||
|
function createSyncIndex(id) {
|
||||||
|
return "sync." + PROVIDER_COUCHDB + "." + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var merge = settings.conflictMode == 'merge';
|
||||||
|
|
||||||
|
function createSyncAttributes(id, rev, content, title, discussionListJSON) {
|
||||||
|
discussionListJSON = discussionListJSON || '{}';
|
||||||
|
var syncAttributes = {};
|
||||||
|
syncAttributes.provider = couchdbProvider;
|
||||||
|
syncAttributes.id = id;
|
||||||
|
syncAttributes.rev = rev;
|
||||||
|
syncAttributes.contentCRC = utils.crc32(content);
|
||||||
|
syncAttributes.titleCRC = utils.crc32(title);
|
||||||
|
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
|
||||||
|
syncAttributes.syncIndex = createSyncIndex(id);
|
||||||
|
if(merge === true) {
|
||||||
|
// Need to store the whole content for merge
|
||||||
|
syncAttributes.content = content;
|
||||||
|
syncAttributes.title = title;
|
||||||
|
syncAttributes.discussionList = discussionListJSON;
|
||||||
|
}
|
||||||
|
return syncAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importFilesFromIds(ids) {
|
||||||
|
couchdbHelper.downloadContent(ids.map(function(id) {
|
||||||
|
return {
|
||||||
|
_id: id
|
||||||
|
};
|
||||||
|
}), function(error, result) {
|
||||||
|
if(error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fileDescList = [];
|
||||||
|
var fileDesc;
|
||||||
|
_.each(result, function(file) {
|
||||||
|
var content = utils.decodeBase64(file._attachments.content.data);
|
||||||
|
var parsedContent = couchdbProvider.parseContent(content);
|
||||||
|
var syncLocations;
|
||||||
|
var syncAttributes = createSyncAttributes(file._id, file._rev, parsedContent.content, file.title, parsedContent.discussionListJSON);
|
||||||
|
syncLocations = {};
|
||||||
|
syncLocations[syncAttributes.syncIndex] = syncAttributes;
|
||||||
|
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
|
||||||
|
fileDescList.push(fileDesc);
|
||||||
|
});
|
||||||
|
if(fileDesc !== undefined) {
|
||||||
|
eventMgr.onSyncImportSuccess(fileDescList, couchdbProvider);
|
||||||
|
fileMgr.selectFile(fileDesc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
couchdbProvider.importFiles = function() {
|
||||||
|
var tag = $('#select-sync-import-couchdb-tag').val();
|
||||||
|
if(!tag) {
|
||||||
|
var ids = _.chain(($('#input-sync-import-couchdb-documentid').val() || '').split(/\s+/))
|
||||||
|
.compact()
|
||||||
|
.unique()
|
||||||
|
.value();
|
||||||
|
var importIds = [];
|
||||||
|
_.each(ids, function(id) {
|
||||||
|
var syncIndex = createSyncIndex(id);
|
||||||
|
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
|
||||||
|
if(fileDesc !== undefined) {
|
||||||
|
return eventMgr.onError('"' + fileDesc.title + '" is already in your local documents.');
|
||||||
|
}
|
||||||
|
importIds.push(id);
|
||||||
|
});
|
||||||
|
importFilesFromIds(importIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) {
|
||||||
|
var data = couchdbProvider.serializeContent(content, discussionListJSON);
|
||||||
|
var tags = frontMatter && frontMatter.tags;
|
||||||
|
couchdbHelper.uploadDocument(undefined, title, data, tags, undefined, function(error, result) {
|
||||||
|
if(error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
var syncAttributes = createSyncAttributes(result.id, result.rev, content, title, discussionListJSON);
|
||||||
|
callback(undefined, syncAttributes);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, frontMatter, syncAttributes, callback) {
|
||||||
|
if(
|
||||||
|
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
|
||||||
|
(syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
|
||||||
|
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
|
||||||
|
) {
|
||||||
|
return callback(undefined, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = couchdbProvider.serializeContent(content, discussionList);
|
||||||
|
var tags = frontMatter && frontMatter.tags;
|
||||||
|
couchdbHelper.uploadDocument(syncAttributes.id, title, data, tags, syncAttributes.rev, function(error, result) {
|
||||||
|
if(error) {
|
||||||
|
return callback(error, true);
|
||||||
|
}
|
||||||
|
syncAttributes.rev = result.rev;
|
||||||
|
if(merge === true) {
|
||||||
|
// Need to store the whole content for merge
|
||||||
|
syncAttributes.content = content;
|
||||||
|
syncAttributes.title = title;
|
||||||
|
syncAttributes.discussionList = discussionList;
|
||||||
|
}
|
||||||
|
syncAttributes.contentCRC = contentCRC;
|
||||||
|
syncAttributes.titleCRC = titleCRC;
|
||||||
|
syncAttributes.discussionListCRC = discussionListCRC;
|
||||||
|
callback(undefined, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
couchdbProvider.syncDown = function(callback) {
|
||||||
|
var lastChangeId = parseInt(storage[PROVIDER_COUCHDB + ".lastChangeId"], 10);
|
||||||
|
var syncLocations = {};
|
||||||
|
_.each(fileSystem, function(fileDesc) {
|
||||||
|
_.each(fileDesc.syncLocations, function(syncAttributes) {
|
||||||
|
syncAttributes.provider === couchdbProvider && (syncLocations[syncAttributes.id] = syncAttributes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
couchdbHelper.checkChanges(lastChangeId, syncLocations, function(error, changes, newChangeId) {
|
||||||
|
if(error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
var interestingChanges = [];
|
||||||
|
_.each(changes, function(change) {
|
||||||
|
var syncIndex = createSyncIndex(change._id);
|
||||||
|
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
|
||||||
|
var syncAttributes = fileDesc && fileDesc.syncLocations[syncIndex];
|
||||||
|
if(!syncAttributes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Store fileDesc and syncAttributes references to avoid 2 times search
|
||||||
|
change.fileDesc = fileDesc;
|
||||||
|
change.syncAttributes = syncAttributes;
|
||||||
|
interestingChanges.push(change);
|
||||||
|
});
|
||||||
|
couchdbHelper.downloadContent(interestingChanges, function(error, changes) {
|
||||||
|
if(error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
function mergeChange() {
|
||||||
|
if(changes.length === 0) {
|
||||||
|
storage[PROVIDER_COUCHDB + ".lastChangeId"] = newChangeId;
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
var change = changes.pop();
|
||||||
|
var fileDesc = change.fileDesc;
|
||||||
|
var syncAttributes = change.syncAttributes;
|
||||||
|
// File deleted
|
||||||
|
if(change.deleted === true) {
|
||||||
|
eventMgr.onError('"' + fileDesc.title + '" has been removed from CouchDB.');
|
||||||
|
fileDesc.removeSyncLocation(syncAttributes);
|
||||||
|
return eventMgr.onSyncRemoved(fileDesc, syncAttributes);
|
||||||
|
}
|
||||||
|
var file = change;
|
||||||
|
var content = utils.decodeBase64(file._attachments.content.data);
|
||||||
|
var parsedContent = couchdbProvider.parseContent(content);
|
||||||
|
var remoteContent = parsedContent.content;
|
||||||
|
var remoteTitle = file.title;
|
||||||
|
var remoteDiscussionListJSON = parsedContent.discussionListJSON;
|
||||||
|
var remoteDiscussionList = parsedContent.discussionList;
|
||||||
|
var remoteCRC = couchdbProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
|
||||||
|
|
||||||
|
// Update syncAttributes
|
||||||
|
syncAttributes.rev = file._rev;
|
||||||
|
if(merge === true) {
|
||||||
|
// Need to store the whole content for merge
|
||||||
|
syncAttributes.content = remoteContent;
|
||||||
|
syncAttributes.title = remoteTitle;
|
||||||
|
syncAttributes.discussionList = remoteDiscussionListJSON;
|
||||||
|
}
|
||||||
|
syncAttributes.contentCRC = remoteCRC.contentCRC;
|
||||||
|
syncAttributes.titleCRC = remoteCRC.titleCRC;
|
||||||
|
syncAttributes.discussionListCRC = remoteCRC.discussionListCRC;
|
||||||
|
utils.storeAttributes(syncAttributes);
|
||||||
|
setTimeout(mergeChange, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(mergeChange, 5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var documentEltTmpl = [
|
||||||
|
'<a href="#" class="list-group-item document clearfix" data-document-id="<%= document._id %>">',
|
||||||
|
'<div class="date pull-right"><%= date %></div></div>',
|
||||||
|
'<div class="name"><i class="icon-provider-couchdb"></i> ',
|
||||||
|
'<%= document.title %></div>',
|
||||||
|
'</a>'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
|
||||||
|
eventMgr.addListener("onReady", function() {
|
||||||
|
var modalElt = document.querySelector('.modal-download-couchdb');
|
||||||
|
var $documentListElt = $(modalElt.querySelector('.list-group.document-list'));
|
||||||
|
var $selectedDocumentListElt = $(modalElt.querySelector('.selected-document-list'));
|
||||||
|
var $pleaseWaitElt = $(modalElt.querySelector('.please-wait'));
|
||||||
|
var $moreDocumentsElt = $(modalElt.querySelector('.more-documents'));
|
||||||
|
var documentMap, lastDocument;
|
||||||
|
var selectedDocuments, $selectedElts;
|
||||||
|
function doSelect() {
|
||||||
|
$selectedElts = $documentListElt.children('.active').clone();
|
||||||
|
selectedDocuments = [];
|
||||||
|
$selectedElts.each(function() {
|
||||||
|
selectedDocuments.push(documentMap[$(this).data('documentId')]);
|
||||||
|
});
|
||||||
|
$selectedDocumentListElt.empty().append($selectedElts);
|
||||||
|
$(modalElt.querySelectorAll('.action-delete-items')).parent().toggleClass('disabled', selectedDocuments.length === 0);
|
||||||
|
}
|
||||||
|
function clear() {
|
||||||
|
documentMap = {};
|
||||||
|
lastDocument = undefined;
|
||||||
|
$documentListElt.empty();
|
||||||
|
doSelect();
|
||||||
|
}
|
||||||
|
clear();
|
||||||
|
function deleteMode(enabled) {
|
||||||
|
$(modalElt.querySelectorAll('.confirm-delete')).toggleClass('hide', !enabled);
|
||||||
|
$(modalElt.querySelectorAll('.document-list')).toggleClass('hide', enabled);
|
||||||
|
}
|
||||||
|
function updateDocumentList() {
|
||||||
|
$pleaseWaitElt.removeClass('hide');
|
||||||
|
$moreDocumentsElt.addClass('hide');
|
||||||
|
couchdbHelper.listDocuments(undefined, lastDocument && lastDocument.updated, function(err, result) {
|
||||||
|
if(err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$pleaseWaitElt.addClass('hide');
|
||||||
|
if(result.length === 3) {
|
||||||
|
$moreDocumentsElt.removeClass('hide');
|
||||||
|
lastDocument = result.pop();
|
||||||
|
}
|
||||||
|
var documentListHtml = _.reduce(result, function(result, document) {
|
||||||
|
documentMap[document._id] = document;
|
||||||
|
return result + _.template(documentEltTmpl, {
|
||||||
|
document: document,
|
||||||
|
date: utils.formatDate(document.updated)
|
||||||
|
});
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
$documentListElt.append(documentListHtml);
|
||||||
|
});
|
||||||
|
deleteMode(false);
|
||||||
|
}
|
||||||
|
$(modalElt)
|
||||||
|
.on('show.bs.modal', updateDocumentList)
|
||||||
|
.on('hidden.bs.modal', clear)
|
||||||
|
.on('click', '.document-list .document', function() {
|
||||||
|
$(this).toggleClass('active');
|
||||||
|
doSelect();
|
||||||
|
})
|
||||||
|
.on('click', '.more-documents', updateDocumentList)
|
||||||
|
.on('click', '.action-unselect-all', function() {
|
||||||
|
$documentListElt.children().removeClass('active');
|
||||||
|
doSelect();
|
||||||
|
})
|
||||||
|
.on('click', '.action-delete-items', function() {
|
||||||
|
doSelect();
|
||||||
|
if($selectedElts.length) {
|
||||||
|
deleteMode(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('click', '.action-delete-items-confirm', function() {
|
||||||
|
couchdbHelper.deleteDocuments(selectedDocuments);
|
||||||
|
clear();
|
||||||
|
updateDocumentList();
|
||||||
|
})
|
||||||
|
.on('click', '.action-cancel', function() {
|
||||||
|
deleteMode(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return couchdbProvider;
|
||||||
|
});
|
@ -94,7 +94,7 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
dropboxProvider.exportFile = function(event, title, content, discussionListJSON, callback) {
|
dropboxProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) {
|
||||||
var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event);
|
var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event);
|
||||||
path = checkPath(path);
|
path = checkPath(path);
|
||||||
if(path === undefined) {
|
if(path === undefined) {
|
||||||
@ -118,7 +118,7 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
|
dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, frontMatter, syncAttributes, callback) {
|
||||||
if(
|
if(
|
||||||
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
|
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
|
||||||
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
|
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
|
||||||
|
@ -12,7 +12,7 @@ define([
|
|||||||
"editor",
|
"editor",
|
||||||
"helpers/googleHelper",
|
"helpers/googleHelper",
|
||||||
"text!html/dialogExportGdrive.html",
|
"text!html/dialogExportGdrive.html",
|
||||||
"text!html/dialogAutoSyncGdrive.html",
|
"text!html/dialogAutoSyncGdrive.html"
|
||||||
], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, editor, googleHelper, dialogExportGdriveHTML, dialogAutoSyncGdriveHTML) {
|
], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, editor, googleHelper, dialogExportGdriveHTML, dialogAutoSyncGdriveHTML) {
|
||||||
|
|
||||||
return function(providerId, providerName, accountIndex) {
|
return function(providerId, providerName, accountIndex) {
|
||||||
@ -29,6 +29,7 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
var merge = settings.conflictMode == 'merge';
|
var merge = settings.conflictMode == 'merge';
|
||||||
|
|
||||||
function createSyncAttributes(id, etag, content, title, discussionListJSON) {
|
function createSyncAttributes(id, etag, content, title, discussionListJSON) {
|
||||||
discussionListJSON = discussionListJSON || '{}';
|
discussionListJSON = discussionListJSON || '{}';
|
||||||
var syncAttributes = {};
|
var syncAttributes = {};
|
||||||
@ -99,7 +100,7 @@ define([
|
|||||||
}, 'doc', accountId);
|
}, 'doc', accountId);
|
||||||
};
|
};
|
||||||
|
|
||||||
gdriveProvider.exportFile = function(event, title, content, discussionListJSON, callback) {
|
gdriveProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) {
|
||||||
var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid');
|
var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid');
|
||||||
if(fileId) {
|
if(fileId) {
|
||||||
// Check that file is not synchronized with another an existing
|
// Check that file is not synchronized with another an existing
|
||||||
@ -122,7 +123,7 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
|
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, frontMatter, syncAttributes, callback) {
|
||||||
if(
|
if(
|
||||||
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
|
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
|
||||||
(syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
|
(syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
|
||||||
@ -227,6 +228,7 @@ define([
|
|||||||
utils.storeAttributes(syncAttributes);
|
utils.storeAttributes(syncAttributes);
|
||||||
setTimeout(mergeChange, 5);
|
setTimeout(mergeChange, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(mergeChange, 5);
|
setTimeout(mergeChange, 5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -357,6 +359,7 @@ define([
|
|||||||
|
|
||||||
storage.removeItem('gdrive.state');
|
storage.removeItem('gdrive.state');
|
||||||
if(state.action == "create") {
|
if(state.action == "create") {
|
||||||
|
eventMgr.onMessage('Please wait while creating your document on ' + providerName);
|
||||||
googleHelper.upload(undefined, state.folderId, constants.GDRIVE_DEFAULT_FILE_TITLE, settings.defaultContent, undefined, undefined, accountId, function(error, file) {
|
googleHelper.upload(undefined, state.folderId, constants.GDRIVE_DEFAULT_FILE_TITLE, settings.defaultContent, undefined, undefined, accountId, function(error, file) {
|
||||||
if(error) {
|
if(error) {
|
||||||
return;
|
return;
|
||||||
@ -381,6 +384,7 @@ define([
|
|||||||
importIds.push(id);
|
importIds.push(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
eventMgr.onMessage('Please wait while loading your document from ' + providerName);
|
||||||
importFilesFromIds(importIds);
|
importFilesFromIds(importIds);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -188,7 +188,7 @@ define([
|
|||||||
function createPublishIndex(fileDesc, publishAttributes) {
|
function createPublishIndex(fileDesc, publishAttributes) {
|
||||||
var publishIndex;
|
var publishIndex;
|
||||||
do {
|
do {
|
||||||
publishIndex = "publish." + utils.randomString();
|
publishIndex = "publish." + utils.id();
|
||||||
} while(_.has(storage, publishIndex));
|
} while(_.has(storage, publishIndex));
|
||||||
publishAttributes.publishIndex = publishIndex;
|
publishAttributes.publishIndex = publishIndex;
|
||||||
fileDesc.addPublishLocation(publishAttributes);
|
fileDesc.addPublishLocation(publishAttributes);
|
||||||
|
@ -455,6 +455,10 @@ kbd {
|
|||||||
background-position: -144px 0;
|
background-position: -144px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-provider-couchdb {
|
||||||
|
background-position: -162px 0;
|
||||||
|
}
|
||||||
|
|
||||||
/*******************
|
/*******************
|
||||||
* RTL
|
* RTL
|
||||||
*******************/
|
*******************/
|
||||||
|
@ -758,6 +758,22 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.file-list .list-group-item {
|
||||||
|
background-color: @transparent;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.input-rename {
|
||||||
|
width: 220px;
|
||||||
|
height: @input-height-slim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
.list-group .list-group-item {
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
}
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -778,28 +794,18 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file-list .list-group-item {
|
.folder, .document {
|
||||||
background-color: @transparent;
|
|
||||||
padding: 0 3px;
|
|
||||||
}
|
|
||||||
margin-bottom: 0;
|
|
||||||
.list-group .list-group-item {
|
|
||||||
border-radius: @border-radius-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: @folder-color;
|
color: @folder-color;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background-color: @transparent;
|
background-color: @transparent;
|
||||||
}
|
}
|
||||||
.input-rename {
|
.name, .date, .file-count {
|
||||||
width: 220px;
|
|
||||||
height: @input-height-slim;
|
|
||||||
}
|
|
||||||
.name, .file-count {
|
|
||||||
padding: 9px 20px 9px 15px;
|
padding: 9px 20px 9px 15px;
|
||||||
}
|
}
|
||||||
|
.date {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1323,6 +1329,9 @@ a {
|
|||||||
.layout-animate & {
|
.layout-animate & {
|
||||||
.transition(350ms ease-in-out all);
|
.transition(350ms ease-in-out all);
|
||||||
}
|
}
|
||||||
|
.layout-vertical & {
|
||||||
|
.box-shadow(inset 0 1px fade(@secondary, 6%));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#preview-contents {
|
#preview-contents {
|
||||||
|
@ -8,6 +8,7 @@ define([
|
|||||||
"fileMgr",
|
"fileMgr",
|
||||||
"classes/Provider",
|
"classes/Provider",
|
||||||
"providers/dropboxProvider",
|
"providers/dropboxProvider",
|
||||||
|
"providers/couchdbProvider",
|
||||||
"providers/gdriveProvider",
|
"providers/gdriveProvider",
|
||||||
"providers/gdrivesecProvider",
|
"providers/gdrivesecProvider",
|
||||||
"providers/gdriveterProvider"
|
"providers/gdriveterProvider"
|
||||||
@ -79,6 +80,7 @@ define([
|
|||||||
|
|
||||||
// Entry point for up synchronization (upload changes)
|
// Entry point for up synchronization (upload changes)
|
||||||
var uploadCycle = false;
|
var uploadCycle = false;
|
||||||
|
|
||||||
function syncUp(callback) {
|
function syncUp(callback) {
|
||||||
var uploadFileList = [];
|
var uploadFileList = [];
|
||||||
|
|
||||||
@ -96,12 +98,14 @@ define([
|
|||||||
return fileUp();
|
return fileUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Here we are freezing the data to make sure it's uploaded consistently
|
||||||
var uploadContent = fileDesc.content;
|
var uploadContent = fileDesc.content;
|
||||||
var uploadContentCRC = utils.crc32(uploadContent);
|
var uploadContentCRC = utils.crc32(uploadContent);
|
||||||
var uploadTitle = fileDesc.title;
|
var uploadTitle = fileDesc.title;
|
||||||
var uploadTitleCRC = utils.crc32(uploadTitle);
|
var uploadTitleCRC = utils.crc32(uploadTitle);
|
||||||
var uploadDiscussionList = fileDesc.discussionListJSON;
|
var uploadDiscussionList = fileDesc.discussionListJSON;
|
||||||
var uploadDiscussionListCRC = utils.crc32(uploadDiscussionList);
|
var uploadDiscussionListCRC = utils.crc32(uploadDiscussionList);
|
||||||
|
var uploadFrontMatter = fileDesc.frontMatter;
|
||||||
|
|
||||||
// Recursive function to upload a single file on multiple locations
|
// Recursive function to upload a single file on multiple locations
|
||||||
function locationUp() {
|
function locationUp() {
|
||||||
@ -121,6 +125,7 @@ define([
|
|||||||
uploadTitleCRC,
|
uploadTitleCRC,
|
||||||
uploadDiscussionList,
|
uploadDiscussionList,
|
||||||
uploadDiscussionListCRC,
|
uploadDiscussionListCRC,
|
||||||
|
uploadFrontMatter,
|
||||||
syncAttributes,
|
syncAttributes,
|
||||||
function(error, uploadFlag) {
|
function(error, uploadFlag) {
|
||||||
if(uploadFlag === true) {
|
if(uploadFlag === true) {
|
||||||
@ -138,6 +143,7 @@ define([
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
locationUp();
|
locationUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +182,7 @@ define([
|
|||||||
providerDown();
|
providerDown();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
providerDown();
|
providerDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +203,7 @@ define([
|
|||||||
var providerList = _.filter(providerMap, function(provider) {
|
var providerList = _.filter(providerMap, function(provider) {
|
||||||
return provider.autosyncConfig.mode == 'all';
|
return provider.autosyncConfig.mode == 'all';
|
||||||
});
|
});
|
||||||
|
|
||||||
function providerAutosync() {
|
function providerAutosync() {
|
||||||
// No more provider
|
// No more provider
|
||||||
if(providerList.length === 0) {
|
if(providerList.length === 0) {
|
||||||
@ -270,27 +278,30 @@ define([
|
|||||||
* Initialize module
|
* Initialize module
|
||||||
**************************************************************************/
|
**************************************************************************/
|
||||||
|
|
||||||
// Initialize the export dialog
|
function loadPreferences(provider, action) {
|
||||||
function initExportDialog(provider) {
|
|
||||||
|
|
||||||
// Reset fields
|
|
||||||
utils.resetModalInputs();
|
utils.resetModalInputs();
|
||||||
|
var preferences = utils.retrieveIgnoreError(provider.providerId + '.' + action + 'Preferences');
|
||||||
// Load preferences
|
if(preferences) {
|
||||||
var exportPreferences = utils.retrieveIgnoreError(provider.providerId + ".exportPreferences");
|
|
||||||
if(exportPreferences) {
|
|
||||||
_.each(provider.exportPreferencesInputIds, function(inputId) {
|
_.each(provider.exportPreferencesInputIds, function(inputId) {
|
||||||
var exportPreferenceValue = exportPreferences[inputId];
|
var exportPreferenceValue = preferences[inputId];
|
||||||
|
var setValue = utils.setInputValue;
|
||||||
if(_.isBoolean(exportPreferenceValue)) {
|
if(_.isBoolean(exportPreferenceValue)) {
|
||||||
utils.setInputChecked("#input-sync-export-" + inputId, exportPreferenceValue);
|
setValue = utils.setInputChecked;
|
||||||
}
|
|
||||||
else {
|
|
||||||
utils.setInputValue("#input-sync-export-" + inputId, exportPreferenceValue);
|
|
||||||
}
|
}
|
||||||
|
setValue('#input-sync-' + action + '-' + inputId, exportPreferenceValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Open dialog
|
// Initialize the import dialog
|
||||||
|
function initImportDialog(provider) {
|
||||||
|
loadPreferences(provider, 'import');
|
||||||
|
$(".modal-download-" + provider.providerId).modal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the export dialog
|
||||||
|
function initExportDialog(provider) {
|
||||||
|
loadPreferences(provider, 'export');
|
||||||
$(".modal-upload-" + provider.providerId).modal();
|
$(".modal-upload-" + provider.providerId).modal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +329,11 @@ define([
|
|||||||
$(".action-sync-import-" + provider.providerId).click(function(event) {
|
$(".action-sync-import-" + provider.providerId).click(function(event) {
|
||||||
provider.importFiles(event);
|
provider.importFiles(event);
|
||||||
});
|
});
|
||||||
// Provider's export action
|
// Provider's import dialog action
|
||||||
|
$(".action-sync-import-dialog-" + provider.providerId).click(function() {
|
||||||
|
initImportDialog(provider);
|
||||||
|
});
|
||||||
|
// Provider's export dialog action
|
||||||
$(".action-sync-export-dialog-" + provider.providerId).click(function() {
|
$(".action-sync-export-dialog-" + provider.providerId).click(function() {
|
||||||
initExportDialog(provider);
|
initExportDialog(provider);
|
||||||
});
|
});
|
||||||
@ -334,7 +349,7 @@ define([
|
|||||||
$(".action-sync-export-" + provider.providerId).click(function(event) {
|
$(".action-sync-export-" + provider.providerId).click(function(event) {
|
||||||
var fileDesc = fileMgr.currentFile;
|
var fileDesc = fileMgr.currentFile;
|
||||||
|
|
||||||
provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, function(error, syncAttributes) {
|
provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, fileDesc.frontMatter, function(error, syncAttributes) {
|
||||||
if(error) {
|
if(error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -87,24 +87,15 @@ define([
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generates a 24 chars length random string (should be enough to prevent collisions)
|
// Generates a 24 char length random id
|
||||||
utils.randomString = (function() {
|
var idAlphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
var max = Math.pow(36, 6);
|
utils.id = function() {
|
||||||
|
var result = [];
|
||||||
function s6() {
|
for(var i = 0; i < 24; i++) {
|
||||||
// Linear [0-9a-z]{6} random string
|
result.push(idAlphabet[Math.random() * idAlphabet.length | 0]);
|
||||||
return ('000000' + (Math.random() * max | 0).toString(36)).slice(-6);
|
|
||||||
}
|
}
|
||||||
|
return result.join('');
|
||||||
return function() {
|
|
||||||
return [
|
|
||||||
s6(),
|
|
||||||
s6(),
|
|
||||||
s6(),
|
|
||||||
s6()
|
|
||||||
].join('');
|
|
||||||
};
|
};
|
||||||
})();
|
|
||||||
|
|
||||||
// Return a parameter from the URL
|
// Return a parameter from the URL
|
||||||
utils.getURLParameter = function(name) {
|
utils.getURLParameter = function(name) {
|
||||||
@ -549,6 +540,122 @@ define([
|
|||||||
return result.join("");
|
return result.join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function padNumber(num, digits, trim) {
|
||||||
|
var neg = '';
|
||||||
|
if (num < 0) {
|
||||||
|
neg = '-';
|
||||||
|
num = -num;
|
||||||
|
}
|
||||||
|
num = '' + num;
|
||||||
|
while(num.length < digits) {
|
||||||
|
num = '0' + num;
|
||||||
|
}
|
||||||
|
if (trim) {
|
||||||
|
num = num.substr(num.length - digits);
|
||||||
|
}
|
||||||
|
return neg + num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateGetter(name, size, offset, trim) {
|
||||||
|
offset = offset || 0;
|
||||||
|
return function(date) {
|
||||||
|
var value = date['get' + name]();
|
||||||
|
if (offset > 0 || value > -offset) {
|
||||||
|
value += offset;
|
||||||
|
}
|
||||||
|
if (value === 0 && offset == -12 ) {
|
||||||
|
value = 12;
|
||||||
|
}
|
||||||
|
return padNumber(value, size, trim);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateStrGetter(name, shortForm) {
|
||||||
|
return function(date, formats) {
|
||||||
|
var value = date['get' + name]();
|
||||||
|
var get = (shortForm ? ('SHORT' + name) : name).toUpperCase();
|
||||||
|
|
||||||
|
return formats[get][value];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/;
|
||||||
|
var DATE_FORMATS = {
|
||||||
|
yyyy: dateGetter('FullYear', 4),
|
||||||
|
yy: dateGetter('FullYear', 2, 0, true),
|
||||||
|
y: dateGetter('FullYear', 1),
|
||||||
|
MMMM: dateStrGetter('Month'),
|
||||||
|
MMM: dateStrGetter('Month', true),
|
||||||
|
MM: dateGetter('Month', 2, 1),
|
||||||
|
M: dateGetter('Month', 1, 1),
|
||||||
|
dd: dateGetter('Date', 2),
|
||||||
|
d: dateGetter('Date', 1),
|
||||||
|
HH: dateGetter('Hours', 2),
|
||||||
|
H: dateGetter('Hours', 1),
|
||||||
|
hh: dateGetter('Hours', 2, -12),
|
||||||
|
h: dateGetter('Hours', 1, -12),
|
||||||
|
mm: dateGetter('Minutes', 2),
|
||||||
|
m: dateGetter('Minutes', 1),
|
||||||
|
ss: dateGetter('Seconds', 2),
|
||||||
|
s: dateGetter('Seconds', 1),
|
||||||
|
// while ISO 8601 requires fractions to be prefixed with `.` or `,`
|
||||||
|
// we can be just safely rely on using `sss` since we currently don't support single or two digit fractions
|
||||||
|
sss: dateGetter('Milliseconds', 3),
|
||||||
|
EEEE: dateStrGetter('Day'),
|
||||||
|
EEE: dateStrGetter('Day', true)
|
||||||
|
};
|
||||||
|
|
||||||
|
var DATETIME_FORMATS = {
|
||||||
|
MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December'.split(','),
|
||||||
|
SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','),
|
||||||
|
DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','),
|
||||||
|
SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','),
|
||||||
|
AMPMS: ['AM','PM'],
|
||||||
|
medium: 'MMM d, y h:mm:ss a',
|
||||||
|
short: 'M/d/yy h:mm a',
|
||||||
|
fullDate: 'EEEE, MMMM d, y',
|
||||||
|
longDate: 'MMMM d, y',
|
||||||
|
mediumDate: 'MMM d, y',
|
||||||
|
shortDate: 'M/d/yy',
|
||||||
|
mediumTime: 'h:mm:ss a',
|
||||||
|
shortTime: 'h:mm a'
|
||||||
|
};
|
||||||
|
|
||||||
|
utils.formatDate = function(date) {
|
||||||
|
var text = '',
|
||||||
|
parts = [],
|
||||||
|
fn, match;
|
||||||
|
|
||||||
|
var interval = Date.now() - date;
|
||||||
|
var format = 'HH:mm';
|
||||||
|
if(interval > 31556940000) {
|
||||||
|
format = 'y';
|
||||||
|
}
|
||||||
|
else if(interval > 86400000) {
|
||||||
|
format = 'MMM d';
|
||||||
|
}
|
||||||
|
date = new Date(date);
|
||||||
|
|
||||||
|
while(format) {
|
||||||
|
match = DATE_FORMATS_SPLIT.exec(format);
|
||||||
|
if (match) {
|
||||||
|
parts = parts.concat(match.slice(1));
|
||||||
|
format = parts.pop();
|
||||||
|
} else {
|
||||||
|
parts.push(format);
|
||||||
|
format = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.forEach(function(value){
|
||||||
|
fn = DATE_FORMATS[value];
|
||||||
|
text += fn ? fn(date, DATETIME_FORMATS)
|
||||||
|
: value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
|
||||||
|
});
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
// Base64 conversion
|
// Base64 conversion
|
||||||
utils.encodeBase64 = function(str) {
|
utils.encodeBase64 = function(str) {
|
||||||
if(str.length === 0) {
|
if(str.length === 0) {
|
||||||
@ -604,6 +711,10 @@ define([
|
|||||||
return x.join('');
|
return x.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
utils.decodeBase64 = function(str) {
|
||||||
|
return window.unescape(decodeURIComponent(window.atob(str)));
|
||||||
|
};
|
||||||
|
|
||||||
// CRC32 algorithm
|
// CRC32 algorithm
|
||||||
var mHash = [
|
var mHash = [
|
||||||
0,
|
0,
|
||||||
|
@ -307,7 +307,7 @@
|
|||||||
<div class="col-md-4 col-md-offset-2">
|
<div class="col-md-4 col-md-offset-2">
|
||||||
<h2 id="fully-customizable">Fully customizable</h2>
|
<h2 id="fully-customizable">Fully customizable</h2>
|
||||||
|
|
||||||
<p>StackEdit has an infinite combinations of settings. Theme, layout, shortcuts can be
|
<p>StackEdit offers infinite combinations of settings. Theme, layout, shortcuts can be
|
||||||
personalized. For the rest, StackEdit gives you the freedom to make your own extension…</p>
|
personalized. For the rest, StackEdit gives you the freedom to make your own extension…</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user