Commit b50e3e28 authored by Sven Franck's avatar Sven Franck

modifications to pass todomvc tests

parent 1af400dd
/*global window, RSVP, FileReader */
/*jslint indent: 2, maxerr: 3, unparam: true */
(function (window, RSVP, FileReader) {
"use strict";
window.loopEventListener = function (target, type, useCapture, callback,
allowDefault) {
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback,
callback_promise;
function cancelResolver() {
if ((callback_promise !== undefined) &&
(typeof callback_promise.cancel === "function")) {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
target.removeEventListener(type, handle_event_callback, useCapture);
}
cancelResolver();
}
function itsANonResolvableTrap(resolve, reject) {
handle_event_callback = function (evt) {
evt.stopPropagation();
if (allowDefault !== true) {
evt.preventDefault();
}
cancelResolver();
callback_promise = new RSVP.Queue()
.push(function () {
return callback(evt);
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(itsANonResolvableTrap, canceller);
};
window.promiseEventListener = function (target, type, useCapture) {
//////////////////////////
// Resolve the promise as soon as the event is triggered
// eventListener is removed when promise is cancelled/resolved/rejected
//////////////////////////
var handle_event_callback;
function canceller() {
target.removeEventListener(type, handle_event_callback, useCapture);
}
function resolver(resolve) {
handle_event_callback = function (evt) {
canceller();
evt.stopPropagation();
evt.preventDefault();
resolve(evt);
return false;
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(resolver, canceller);
};
window.promiseReadAsText = function (file) {
return new RSVP.Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function (evt) {
resolve(evt.target.result);
};
reader.onerror = function (evt) {
reject(evt);
};
reader.readAsText(file);
});
};
}(window, RSVP, FileReader));
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<title>Router Gadget</title> <title>Router Gadget</title>
<script src="rsvp.js"></script> <script src="rsvp.js"></script>
<script src="renderjs.js"></script> <script src="renderjs.js"></script>
<script src="gadget_global.js"></script>
<script src="gadget_router.js"></script> <script src="gadget_router.js"></script>
</head> </head>
<body> <body>
......
/*global define, App, window, RSVP, rJS */ /*global define, App, window, RSVP, rJS, loopEventListener */
/*jshint unused:false */ /*jshint unused:false */
(function (window, RSVP, rJS) { (function (window, RSVP, rJS, loopEventListener) {
'use strict'; 'use strict';
...@@ -16,66 +16,6 @@ ...@@ -16,66 +16,6 @@
} }
} }
// Return an infinite promise for event listening purposes.
// Copied directly from RenderJS implementation; don't worry about it.
function loopEventListener(
target, type, useCapture, callback, prevent_default) {
var handle_event_callback;
var callback_promise;
if (prevent_default === undefined) {
prevent_default = true;
}
function cancelResolver() {
if ((callback_promise !== undefined) &&
(typeof callback_promise.cancel === 'function')) {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
target.removeEventListener(
type, handle_event_callback, useCapture);
}
cancelResolver();
}
function itsANonResolvableTrap(resolve, reject) {
var result;
handle_event_callback = function (evt) {
if (prevent_default) {
evt.stopPropagation();
evt.preventDefault();
}
cancelResolver();
try {
result = callback(evt);
} catch (e) {
result = RSVP.reject(e);
}
callback_promise = result;
new RSVP.Queue()
.push(function () {
return result;
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(itsANonResolvableTrap, canceller);
}
rJS(window) rJS(window)
// Initialize the gadget as soon as it is loaded in memory, // Initialize the gadget as soon as it is loaded in memory,
...@@ -101,4 +41,4 @@ ...@@ -101,4 +41,4 @@
// Declare an acquired method from the parent gadget to use it. // Declare an acquired method from the parent gadget to use it.
.declareAcquiredMethod('setQuery', 'setQuery'); .declareAcquiredMethod('setQuery', 'setQuery');
}(window, RSVP, rJS)); }(window, RSVP, rJS, loopEventListener));
\ No newline at end of file
...@@ -17,7 +17,8 @@ button { ...@@ -17,7 +17,8 @@ button {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
body { body {
...@@ -29,12 +30,14 @@ body { ...@@ -29,12 +30,14 @@ body {
max-width: 550px; max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300; font-weight: 300;
} }
:focus { button,
outline: 0; input[type="checkbox"] {
outline: none;
} }
.hidden { .hidden {
...@@ -90,13 +93,15 @@ body { ...@@ -90,13 +93,15 @@ body {
font-weight: inherit; font-weight: inherit;
line-height: 1.4em; line-height: 1.4em;
border: 0; border: 0;
outline: none;
color: inherit; color: inherit;
padding: 6px; padding: 6px;
border: 1px solid #999; border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
.new-todo { .new-todo {
...@@ -112,32 +117,28 @@ body { ...@@ -112,32 +117,28 @@ body {
border-top: 1px solid #e6e6e6; border-top: 1px solid #e6e6e6;
} }
.toggle-all { label[for='toggle-all'] {
text-align: center; display: none;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
} }
.toggle-all + label { .toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px; width: 60px;
height: 34px; height: 34px;
font-size: 0; text-align: center;
position: absolute; border: none; /* Mobile Safari */
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
} }
.toggle-all + label:before { .toggle-all:before {
content: '❯'; content: '❯';
font-size: 22px; font-size: 22px;
color: #e6e6e6; color: #e6e6e6;
padding: 10px 27px 10px 27px; padding: 10px 27px 10px 27px;
} }
.toggle-all:checked + label:before { .toggle-all:checked:before {
color: #737373; color: #737373;
} }
...@@ -165,7 +166,7 @@ body { ...@@ -165,7 +166,7 @@ body {
.todo-list li.editing .edit { .todo-list li.editing .edit {
display: block; display: block;
width: 506px; width: 506px;
padding: 12px 16px; padding: 13px 17px 12px 17px;
margin: 0 0 0 43px; margin: 0 0 0 43px;
} }
...@@ -187,27 +188,19 @@ body { ...@@ -187,27 +188,19 @@ body {
appearance: none; appearance: none;
} }
.todo-list li .toggle { .todo-list li .toggle:after {
opacity: 0; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
} }
.todo-list li .toggle:checked + label { .todo-list li .toggle:checked:after {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
} }
.todo-list li label { .todo-list li label {
white-space: pre-line;
word-break: break-all; word-break: break-all;
padding: 15px 15px 15px 60px; padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block; display: block;
line-height: 1.2; line-height: 1.2;
transition: color 0.4s; transition: color 0.4s;
...@@ -307,6 +300,7 @@ body { ...@@ -307,6 +300,7 @@ body {
border-radius: 3px; border-radius: 3px;
} }
.filters li a.selected,
.filters li a:hover { .filters li a:hover {
border-color: rgba(175, 47, 47, 0.1); border-color: rgba(175, 47, 47, 0.1);
} }
...@@ -322,6 +316,7 @@ html .clear-completed:active { ...@@ -322,6 +316,7 @@ html .clear-completed:active {
line-height: 20px; line-height: 20px;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
position: relative;
} }
.clear-completed:hover { .clear-completed:hover {
...@@ -363,6 +358,13 @@ html .clear-completed:active { ...@@ -363,6 +358,13 @@ html .clear-completed:active {
.todo-list li .toggle { .todo-list li .toggle {
height: 40px; height: 40px;
} }
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
} }
@media (max-width: 430px) { @media (max-width: 430px) {
......
...@@ -3,36 +3,35 @@ ...@@ -3,36 +3,35 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>OfficeJS App</title> <title>TodoMVC App</title>
<link href="base.css" rel="stylesheet">
<link href="index.css" rel="stylesheet">
<script src="rsvp.js"></script> <script src="rsvp.js"></script>
<script src="renderjs.js"></script> <script src="renderjs.js"></script>
<script src="jio.js"></script> <script src="jio.js"></script>
<script src="handlebars.js"></script> <script src="handlebars.js"></script>
<script src="gadget_global.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
<link href="base.css" rel="stylesheet">
<link href="index.css" rel="stylesheet"> <script id="list_template" type="text/x-handlebars-template">
<link href="manifest.json" rel="manifest"> <section id="main" class="main {{#unless todo_exists}}hidden{{/unless}}">
</head>
<body>
<div data-gadget-url="gadget_model.html"
data-gadget-scope="model"
data-gadget-sandbox="public">
</div>
<main class="handlebars">
</main>
<script class="handlebars-template" type="text/x-handlebars-template">
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<form>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</form>
</header>
<section class="main {{#unless todo_exists}}hidden{{/unless}}">
<input class="toggle-all" type="checkbox" {{#if all_completed}}checked="true"{{/if}}>
<label for="toggle-all" class="toggle-label">Mark all as complete</label> <label for="toggle-all" class="toggle-label">Mark all as complete</label>
<ul class="todo-list"> <input id="toggle-all" class="toggle-all" type="checkbox" {{#if all_checked}}checked="true"{{/if}}>
{{#each todo_list}} <ul id="todo-list" class="todo-list"></ul>
</section>
<footer id="footer" class="footer {{#unless todo_exists}}hidden{{/unless}}">
<span id="todo-count" class="todo-count">{{todo_count}}</span>
<ul id="filters" class="filters">
<li><a href="#/">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button id="clear-completed" class="clear-completed {{#if all_completed}}hidden{{/if}}">Clear completed</button>
</footer>
</script>
<script id="item_template" type="text/x-handlebars-template">
<li class="todo-item {{#if this.completed}}completed{{/if}} {{#if this.editing}}editing{{/if}}" <li class="todo-item {{#if this.completed}}completed{{/if}} {{#if this.editing}}editing{{/if}}"
data-jio-id="{{this.id}}"> data-jio-id="{{this.id}}">
<div class="view {{#if this.edit}}hidden{{/if}}"> <div class="view {{#if this.edit}}hidden{{/if}}">
...@@ -42,22 +41,27 @@ ...@@ -42,22 +41,27 @@
</div> </div>
<input class="edit{{#unless this.editing}} hidden{{/unless}}"> <input class="edit{{#unless this.editing}} hidden{{/unless}}">
</li> </li>
{{/each}} </script>
</ul>
</section> </head>
<footer class="footer {{#unless todo_exists}}hidden{{/unless}}"> <body>
<span class="todo-count">{{todo_count}}</span> <section id="todoapp" class="todoapp">
<div class="filters"> <header id="header" class="header">
<a href="#/" class="selected">All</a> <h1>todos</h1>
<a href="#/active">Active</a> <form id="todo-form">
<a href="#/completed">Completed</a> <input id="new-todo" class="new-todo" placeholder="What needs to be done?" autofocus>
</div> </form>
<button class="clear-completed">Clear completed</button> </header>
</footer> <main class="handlebars">
</main>
</section> </section>
<footer class="info"> <footer id="info" class="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
</footer> </footer>
</script>
<div data-gadget-url="gadget_model.html"
data-gadget-scope="model"
data-gadget-sandbox="public">
</div>
</body> </body>
</html> </html>
/*global define, App, window, document, rJS, Handlebars */ /*global window, rJS, Handlebars, RSVP, Boolean, promiseEventListener */
/*jshint unused:false */ /*jshint unused:false */
(function (window, document, rJS, Handlebars) { (function(window, rJS, RSVP, Handlebars, Boolean, promiseEventListener) {
'use strict'; 'use strict';
/////////////////////////////
// Constants // parameters
/////////////////////////////
var SELECTED = 'selected';
var HREF = 'href';
var ARR = [];
var JIO_ID = 'data-jio-id';
var UL = 'ul';
var LI = 'li';
var A = 'a';
var LABEL = 'label';
var DIV = 'div';
var DIVI = 'div input';
var DIVPI = 'div + input';
var VIEW = 'view';
var STR = '';
var TODO = 'todo-item';
var COMPLETED = ' completed';
var FILTERS = '.filters a';
var EDITING = ' editing';
var HIDDEN = 'hidden';
var ITEM = ' item left';
var ITEMS = ' items left';
var INPUT_SELECTOR = '.new-todo';
var ALL = '.toggle-all';
var MAIN = '.main';
var FOOT = '.footer';
var COUNT = '.todo-count';
var SPACE = ' ';
var LIST = '.todo-list';
var EDIT = 'edit';
var ENTER_KEY = 13; var ENTER_KEY = 13;
var ESCAPE_KEY = 27; var ESCAPE_KEY = 27;
var DONE = 'completed';
var CLEAR = '.clear-completed';
/////////////////////////////
// methods
/////////////////////////////
function getId(element, traverse) {
switch (traverse) {
case 2:
return element.parentElement.parentElement.getAttribute(JIO_ID);
case 1:
return element.parentElement.getAttribute(JIO_ID);
}
}
function getAllChecked(list) {
return list.reduce(function(pass, item) {
if (pass === false) {
return false;
}
if (item.completed) {
return true;
}
return false;
}, true);
}
function getElems(element, traverse, target) {
switch (traverse) {
case 2:
return element.parentElement
.parentElement.querySelectorAll(target);
case 0:
return element.querySelectorAll(target);
}
}
function getElem(element, target) {
return element.querySelector(target);
}
function makeList(nodeList) {
return ARR.slice.call(nodeList);
}
function setTodo(node, item, template) {
var faux_list = document.createElement(UL);
faux_list.innerHTML = template(item);
node.appendChild(faux_list.firstElementChild);
}
function setSelectedClass(element) {
makeList(getElems(element, 2, A))
.forEach(function(link) {
link.classList.remove(SELECTED);
if (link.getAttribute(HREF) === element.getAttribute(HREF)) {
link.classList.add(SELECTED);
}
});
}
function removeFromList(list, id) {
list.forEach(function(item, index, array) {
if (array[index].id === id) {
array.splice(index, 1);
}
});
}
// Global Variables function getObj(list, id) {
var handlebars_template; // = Handlebars.compile(template.innerHTML); return list.map(function(item) {
if (item.id === id) {
return item;
}
});
}
function setItemClass(o) {
return TODO +
(o.completed ? COMPLETED : STR) +
(o.editing ? EDITING : STR);
}
function setHidden(root, hide) {
return root + (hide ? (SPACE + HIDDEN) : STR);
}
function setSelector(id) {
return 'li[' + JIO_ID + '="' + id + '"] .edit';
}
// let's go
rJS(window) rJS(window)
// Initiaize the state of the gadget as soon as it is loaded in memory. /////////////////////////////
// state
/////////////////////////////
.setState({ .setState({
create: false,
update: false, update: false,
clear_input: false, clear_input: false,
editing_jio_id: '', editing_jio_id: '',
query: '' query: ''
}) })
// Initialize the gadget as soon as it is loaded in the DOM, /////////////////////////////
// but only after ready() has finished in its child gadgets. // ready
.declareService(function () { /////////////////////////////
.ready(function() {
var gadget = this; var gadget = this;
var temp = gadget.element.querySelector('.handlebars-template');
// Create a new empty element for the router gadget. // initialize the router and set the model on the main gadget for
var div = document.createElement('div'); // easier reference. Set templates, then render first DOM.
return new RSVP.Queue()
.push(function() {
return gadget.getDeclaredGadget('model');
})
.push(function(response) {
var div = document.createElement(DIV);
gadget.element.appendChild(div); gadget.element.appendChild(div);
gadget.property_dict = {
// Compile the Handlebars template only once, on page load. 'model': response
handlebars_template = Handlebars.compile(temp.innerHTML); };
// Declare the router gadget in JavaScript instead of HTML.
return gadget.declareGadget('gadget_router.html', { return gadget.declareGadget('gadget_router.html', {
scope: 'router', scope: 'router',
sandbox: 'public', sandbox: 'public',
element: div element: div
});
}) })
// Render the state for the first time. .push(function() {
.push(function () { gadget.template_dict = {
return gadget.changeState({update: true}); 'list_template': Handlebars.compile(
document.getElementById('list_template').innerHTML
),
'item_template': Handlebars.compile(
document.getElementById('item_template').innerHTML
)
};
return gadget.changeState({
'create': true
});
}); });
}) })
/////////////////////////////
// published methods
/////////////////////////////
// Declare an acquirable method to allow child gadgets to use it. // router calls to update the DOM land here
.allowPublicAcquisition('setQuery', function (param_list) { .allowPublicAcquisition('setQuery', function(param_list) {
var gadget = this; this.changeState({
'query': param_list[0],
// Keep the given query in the state when the router gadget calls. 'update': true
gadget.changeState({query: param_list[0]}); });
}) })
/////////////////////////////
// Render the entire todo app every time the state changes. // published methods
.onStateChange(function (modification_dict) { /////////////////////////////
.declareMethod("storeItem", function(item, jio_id) {
var gadget = this; var gadget = this;
var model_gadget; var model = gadget.property_dict.model;
var todo_count_dict; if (!item) {
return;
// Get the model gadget and todo count dict to store for later. }
return gadget.getDeclaredGadget('model') return new RSVP.Queue()
.push(function (subgadget) { .push(function() {
model_gadget = subgadget; if (jio_id) {
return model_gadget.getTodoCountDict(); return model.putTodo(jio_id, item);
}
return model.postTodo(item);
}) })
.push(function (count_dict) { .push(function() {
todo_count_dict = count_dict; return gadget.changeState({
return model_gadget.getTodos(gadget.state.query); 'clear_input': true,
'update': true
});
});
}) })
// Get the list of todos from storage. /////////////////////////////
.push(function (todo_list) { // onStateChange
var plural = todo_list.length === 1 ? ' item' : ' items'; /////////////////////////////
var focus_query = '.new-todo'; .onStateChange(function(modification_dict) {
var edit_value = ''; var gadget = this;
var post_value = ''; var state = gadget.state;
var dict = gadget.property_dict;
var temp = gadget.template_dict;
var element = gadget.element;
// fetch counter dict and todos
return new RSVP.Queue()
.push(function() {
return RSVP.all([
dict.model.getTodoCountDict(),
dict.model.getTodos(gadget.state.query)
]);
})
.push(function(response_list) {
var count_dict = response_list[0];
var todo_list = response_list[1];
var plural = todo_list.length === 1 ? ITEM : ITEMS;
var focus_selector = INPUT_SELECTOR;
var edit_value = STR;
var input_value = STR;
var toggle_all = getElem(element, ALL);
var all_checked = false;
var count_content = count_dict.active.toString() + plural;
var all_completed = count_dict.active === count_dict.total;
// tick all
if (toggle_all) {
toggle_all.checked =
all_checked =
getAllChecked(todo_list);
}
// If a todo is currently being edited, // keep focus on todo being edited
// set the focus to its edit input. if (state.editing_jio_id) {
// Otherwise, the focus remains on the new todo input. focus_selector = setSelector(state.editing_jio_id);
if (gadget.state.editing_jio_id) {
focus_query = 'li[data-jio-id="' +
gadget.state.editing_jio_id + '"] .edit';
} }
// If the new todo input has not yet been submitted and // set todo being edited
// it exists, then keep its current value in post_value. todo_list.forEach(function(todo) {
if (todo.id === state.editing_jio_id) {
todo.editing = true;
edit_value = todo.title;
} else {
todo.editing = false;
}
});
// clear input
if (!modification_dict.hasOwnProperty('clear_input') && if (!modification_dict.hasOwnProperty('clear_input') &&
gadget.element.querySelector('.new-todo')) { getElem(element, INPUT_SELECTOR)) {
post_value = input_value = getElem(element, INPUT_SELECTOR).value;
gadget.element.querySelector('.new-todo').value; }
// set initial DOM
if (modification_dict.hasOwnProperty('create')) {
getElem(element, '.handlebars').innerHTML =
temp.list_template({
'todo_exists': count_dict.total >= 1,
'todo_count': count_content,
'all_completed': all_completed,
'all_checked': getAllChecked(todo_list)
});
dict.list = getElem(element, LIST);
todo_list.forEach(function(item) {
setTodo(dict.list, item, temp.item_template);
});
} }
// Set at most one todo as currently being edited. // Update DOM
for (var i = 0; i < todo_list.length; i += 1) { if (modification_dict.hasOwnProperty('update') &&
if (todo_list[i].id === gadget.state.editing_jio_id) { dict.list)
todo_list[i].editing = true; {
edit_value = todo_list[i].title; makeList(getElems(dict.list, 0, LI))
.forEach(function(li) {
var jio_id = li.getAttribute(JIO_ID);
var obj = getObj(todo_list, jio_id).filter(Boolean).pop();
if (obj === undefined) {
if (!gadget.state.query) {
li.parentElement.removeChild(li);
} else { } else {
todo_list[i].editing = false; li.classList.add(HIDDEN);
} }
} else {
li.classList.remove(HIDDEN);
li.className = setItemClass(obj);
getElem(li, LABEL).textContent = obj.title;
getElem(li, DIV).className = setHidden(VIEW, obj.edit);
getElem(li, DIVI).checked = obj.completed;
getElem(li, DIVPI).className = setHidden(EDIT, !obj.editing);
} }
removeFromList(todo_list, jio_id);
return;
});
// Apply the Handlebars template on the todo list. // new items
gadget.element.querySelector('.handlebars').innerHTML = todo_list.forEach(function(item) {
handlebars_template({ setTodo(dict.list, item, temp.item_template);
todo_list: todo_list,
todo_exists: todo_count_dict.total >= 1,
todo_count:
todo_count_dict.active.toString() + plural,
all_completed: todo_count_dict.active === 0
}); });
// Focus the proper element and copy the previous values // clear completed
// of the currently editing input and the new todo input if (all_completed) {
// back into them, since Handlebars reset all input values getElem(element, CLEAR).classList.add(HIDDEN);
gadget.element.querySelector(focus_query).focus(); } else {
getElem(element, CLEAR).classList.remove(HIDDEN);
}
// counter
if (count_dict.total === 0) {
getElem(element, MAIN).classList.add(HIDDEN);
getElem(element, FOOT).classList.add(HIDDEN);
} else {
getElem(element, MAIN).classList.remove(HIDDEN);
getElem(element, FOOT).classList.remove(HIDDEN);
}
getElem(element, COUNT).textContent = count_content;
}
// if editing, set focus and input value
if (edit_value) { if (edit_value) {
gadget.element.querySelector(focus_query).value = getElem(element, focus_selector).focus();
edit_value; getElem(element, focus_selector).value = edit_value;
} }
if (post_value) {
gadget.element.querySelector('.new-todo').value = // set filter
post_value; makeList(getElems(element, 0, FILTERS))
.forEach(function(filter) {
filter.classList.remove(SELECTED);
if (filter.getAttribute(HREF) === window.location.hash) {
filter.classList.add(SELECTED);
} }
});
// set todo input value
getElem(element, INPUT_SELECTOR).value = input_value;
gadget.state.update = false; gadget.state.update = false;
gadget.state.clear_input = false; gadget.state.clear_input = false;
return;
}); });
}) })
/////////////////////////////
// onEvent
/////////////////////////////
// new todo
.onEvent('submit', function(event) {
return this.storeItem(event.target.elements[0].value.trim());
}, false, true)
// Post a new todo when the new todo input is submitted. // edit todo
.onEvent('submit', function (event) { .onEvent('dblclick', function(event) {
var gadget = this; var gadget = this;
var item = event.target.elements[0].value.trim(); var dict = gadget.property_dict;
var target = event.target;
var jio_id = getId(event.target, 2);
var input = target.parentElement.nextElementSibling;
// Trim the input and reject blank values. if (target.className !== 'todo-label') {
if (!item) {
return; return;
} }
// Change clear_input in state to clear the new todo input. return new RSVP.Queue()
return gadget.getDeclaredGadget('model') .push(function() {
.push(function (model_gadget) { return gadget.changeState({
return model_gadget.postTodo(item); 'update': true,
}) 'editing_jio_id': jio_id
.push(function () {
return gadget.changeState({clear_input: true});
}); });
}, false, true) })
.push(function() {
// Do the correct action when anything is clicked once. dict.defer = new RSVP.defer();
.onEvent('click', function (event) {
var gadget = this; // ESC resolves the defer and prevents storing
var todo_item = event.target.parentElement.parentElement; return RSVP.any([
var jio_id = todo_item.getAttribute('data-jio-id'); dict.defer.promise,
promiseEventListener(input, 'blur', true)
// Delegate all responsibility to the model gadget. ]);
return gadget.getDeclaredGadget('model') })
.push(function (model_gadget) { .push(function(event) {
switch (event.target.className) { var target;
// Set completed to the opposite of its current status.
case 'toggle':
return model_gadget.toggleOne(
jio_id,
!todo_item.classList.contains('completed')
);
// Set completed to the state of the toggle all checkbox.
case 'toggle-all':
return model_gadget.toggleAll(event.target.checked);
// Set completed to the opposite of the current state
// of the checkbox when the label is clicked, because
// the event is sent before the checkbox changes state
case 'toggle-label':
return model_gadget.toggleAll(
!gadget.element
.querySelector('.toggle-all').checked
);
// Remove the todo with the given ID.
case 'destroy':
return model_gadget.removeOne(jio_id);
// Remove all completed todos.
case 'clear-completed':
return model_gadget.removeCompleted();
// If the user clicked anywhere else, don't do anything, // defer was here
// unless it was outside the currently editing input. if (!event) {
default: input.blur();
if (gadget.state.editing_jio_id && return;
event.target.className !== 'edit') {
return 'cancel editing';
} }
return 'default';
target = event.target;
if (target.value === '') {
return dict.model.removeOne(getId(target, 1));
} }
return gadget.storeItem({
'title': target.value.trim()
}, jio_id);
}) })
.push(function() {
// Only update the user if the user clicked outside the default.
.push(function (path) {
if (path !== 'default') {
return gadget.changeState({ return gadget.changeState({
update: true, 'update': true,
editing_jio_id: '' 'editing_jio_id': ''
}); });
}
}); });
}, false, false) }, false, false)
// Do the correct action when anything is clicked twice. // key inputs
.onEvent('dblclick', function (event) { .onEvent('keydown', function(event) {
var gadget = this; var gadget = this;
var dict = gadget.property_dict;
var target = event.target;
var item;
var jio_id;
// If a todo label is clicked twice, then edit it. if (target.className !== EDIT) {
if (event.target.className === 'todo-label') { return;
return gadget.changeState({
editing_jio_id: event.target.parentElement
.parentElement.getAttribute('data-jio-id')
});
} }
}, false, false)
// Do the correct action when any keys are pressed in an input.
.onEvent('keydown', function (event) {
var gadget = this;
// Reset everything if the escape key is pressed.
if (event.target.className === 'edit') {
if (event.keyCode === ESCAPE_KEY) { if (event.keyCode === ESCAPE_KEY) {
return new RSVP.Queue()
.push(function() {
return gadget.changeState({ return gadget.changeState({
update: true, 'update': true,
editing_jio_id: '' 'editing_jio_id': STR
});
})
.push(function() {
if (dict.defer) {
dict.defer.resolve();
}
}); });
} }
// Change the title of the todo if the enter key is pressed. if (event.keyCode === ENTER_KEY) {
var item = event.target.value.trim(); item = target.value.trim();
if (event.keyCode === ENTER_KEY && item) { if (item) {
return gadget.getDeclaredGadget('model') jio_id = getId(target, 1);
.push(function (model_gadget) { return new RSVP.Queue()
return model_gadget.changeTitle( .push(function() {
event.target.parentElement return dict.model.changeTitle(jio_id, item);
.getAttribute('data-jio-id'),
item
);
}) })
.push(function () { .push(function() {
return gadget.changeState({ return gadget.changeState({
update: true, 'update': true,
editing_jio_id: '' 'editing_jio_id': STR
}); });
}); });
} }
return gadget.changeState({
'update': true,
'editing_jio_id': STR
});
} }
}, false, false)
// clickediclick
.onEvent('click', function(event) {
var gadget = this;
return new RSVP.Queue()
.push(function() {
var model = gadget.property_dict.model;
var target = event.target;
var item = target.parentElement.parentElement;
switch (target.className) {
case 'toggle':
return model.toggleOne(
getId(target, 2), !item.classList.contains(DONE)
);
case 'toggle-all':
return model.toggleAll(target.checked);
case 'destroy':
return model.removeOne(item.getAttribute(JIO_ID));
case 'clear-completed':
return model.removeCompleted();
// filters and exiting edit todo via click
default:
if (target.getAttribute(HREF)) {
setSelectedClass(target);
}
if (gadget.state.editing_jio_id &&
target.className !== EDIT)
{
document.activeElement.blur();
return true;
}
}
})
.push(function(response) {
if (response) {
return gadget.changeState({
'update': true,
'editing_jio_id': ''
});
}
});
}, false, false); }, false, false);
}(window, document, rJS, Handlebars)); }(window, rJS, RSVP, Handlebars, Boolean, promiseEventListener));
...@@ -5716,7 +5716,7 @@ case 5: case 8: case 11: case 14: case 16: ...@@ -5716,7 +5716,7 @@ case 5: case 8: case 11: case 14: case 16:
this.$ = $$[$0]; this.$ = $$[$0];
break; break;
case 6: case 6:
this.$ = mkComplexQuery('OR', [$$[$0-1], $$[$0]]); this.$ = mkComplexQuery('AND', [$$[$0-1], $$[$0]]);
break; break;
case 7: case 7:
this.$ = mkComplexQuery('OR', [$$[$0-2], $$[$0]]); this.$ = mkComplexQuery('OR', [$$[$0-2], $$[$0]]);
...@@ -6683,7 +6683,7 @@ return new Parser; ...@@ -6683,7 +6683,7 @@ return new Parser;
return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$"); return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$");
} }
return new RegExp("^" + stringEscapeRegexpCharacters(string) return new RegExp("^" + stringEscapeRegexpCharacters(string)
.replace(regexp_percent, '.*') .replace(regexp_percent, '[\\s\\S]*')
.replace(regexp_underscore, '.') + "$"); .replace(regexp_underscore, '.') + "$");
} }
...@@ -6985,7 +6985,8 @@ return new Parser; ...@@ -6985,7 +6985,8 @@ return new Parser;
matchMethod = null, matchMethod = null,
operator = this.operator, operator = this.operator,
value = null, value = null,
key = this.key; key = this.key,
k;
if (!(regexp_comparaison.test(operator))) { if (!(regexp_comparaison.test(operator))) {
// `operator` is not correct, we have to change it to "like" or "=" // `operator` is not correct, we have to change it to "like" or "="
...@@ -7004,6 +7005,22 @@ return new Parser; ...@@ -7004,6 +7005,22 @@ return new Parser;
key = this._key_schema.key_set[key]; key = this._key_schema.key_set[key];
} }
// match with all the fields if key is empty
if (key === '') {
matchMethod = this.like;
value = '%' + this.value + '%';
for (k in item) {
if (item.hasOwnProperty(k)) {
if (k !== '__id' && item[k]) {
if (matchMethod(item[k], value) === true) {
return true;
}
}
}
}
return false;
}
if (typeof key === 'object') { if (typeof key === 'object') {
checkKey(key); checkKey(key);
object_value = item[key.read_from]; object_value = item[key.read_from];
...@@ -8428,6 +8445,15 @@ return new Parser; ...@@ -8428,6 +8445,15 @@ return new Parser;
CONFLICT_KEEP_REMOTE = 2, CONFLICT_KEEP_REMOTE = 2,
CONFLICT_CONTINUE = 3; CONFLICT_CONTINUE = 3;
function SkipError(message) {
if ((message !== undefined) && (typeof message !== "string")) {
throw new TypeError('You must pass a string.');
}
this.message = message || "Skip some asynchronous code";
}
SkipError.prototype = new Error();
SkipError.prototype.constructor = SkipError;
/**************************************************** /****************************************************
Use a local jIO to read/write/search documents Use a local jIO to read/write/search documents
Synchronize in background those document with a remote jIO. Synchronize in background those document with a remote jIO.
...@@ -8446,22 +8472,41 @@ return new Parser; ...@@ -8446,22 +8472,41 @@ return new Parser;
function ReplicateStorage(spec) { function ReplicateStorage(spec) {
this._query_options = spec.query || {}; this._query_options = spec.query || {};
if (spec.signature_hash_key !== undefined) {
this._query_options.select_list = [spec.signature_hash_key];
}
this._signature_hash_key = spec.signature_hash_key;
this._local_sub_storage = jIO.createJIO(spec.local_sub_storage); this._local_sub_storage = jIO.createJIO(spec.local_sub_storage);
this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage); this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage);
if (spec.hasOwnProperty('signature_sub_storage')) {
this._signature_sub_storage = jIO.createJIO(spec.signature_sub_storage);
this._custom_signature_sub_storage = true;
} else {
this._signature_hash = "_replicate_" + generateHash( this._signature_hash = "_replicate_" + generateHash(
stringify(spec.local_sub_storage) + stringify(spec.local_sub_storage) +
stringify(spec.remote_sub_storage) + stringify(spec.remote_sub_storage) +
stringify(this._query_options) stringify(this._query_options)
); );
this._signature_sub_storage = jIO.createJIO({ this._signature_sub_storage = jIO.createJIO({
type: "query",
sub_storage: {
type: "document", type: "document",
document_id: this._signature_hash, document_id: this._signature_hash,
sub_storage: spec.signature_storage || spec.local_sub_storage sub_storage: spec.local_sub_storage
}
}); });
this._custom_signature_sub_storage = false;
}
this._use_remote_post = spec.use_remote_post || false; this._use_remote_post = spec.use_remote_post || false;
// Number of request we allow browser execution for attachments
this._parallel_operation_attachment_amount =
spec.parallel_operation_attachment_amount || 1;
// Number of request we allow browser execution for documents
this._parallel_operation_amount =
spec.parallel_operation_amount || 1;
this._conflict_handling = spec.conflict_handling || 0; this._conflict_handling = spec.conflict_handling || 0;
// 0: no resolution (ie, throw an Error) // 0: no resolution (ie, throw an Error)
...@@ -8596,16 +8641,55 @@ return new Parser; ...@@ -8596,16 +8641,55 @@ return new Parser;
arguments); arguments);
}; };
ReplicateStorage.prototype.repair = function () { function dispatchQueue(context, function_used, argument_list,
var context = this, number_queue) {
argument_list = arguments, var result_promise_list = [],
skip_document_dict = {}; i;
// Do not sync the signature document function pushAndExecute(queue) {
skip_document_dict[context._signature_hash] = null; queue
.push(function () {
if (argument_list.length > 0) {
var argument_array = argument_list.shift(),
sub_queue = new RSVP.Queue();
argument_array[0] = sub_queue;
function_used.apply(context, argument_array);
pushAndExecute(queue);
return sub_queue;
}
});
}
for (i = 0; i < number_queue; i += 1) {
result_promise_list.push(new RSVP.Queue());
pushAndExecute(result_promise_list[i]);
}
if (number_queue > 1) {
return RSVP.all(result_promise_list);
}
return result_promise_list[0];
}
function callAllDocsOnStorage(context, storage, cache, cache_key) {
return new RSVP.Queue()
.push(function () {
if (!cache.hasOwnProperty(cache_key)) {
return storage.allDocs(context._query_options)
.push(function (result) {
var i,
cache_entry = {};
for (i = 0; i < result.data.total_rows; i += 1) {
cache_entry[result.data.rows[i].id] = result.data.rows[i].value;
}
cache[cache_key] = cache_entry;
});
}
})
.push(function () {
return cache[cache_key];
});
}
function propagateAttachmentDeletion(skip_attachment_dict, function propagateAttachmentDeletion(context, skip_attachment_dict,
destination, destination,
id, name) { id, name) {
return destination.removeAttachment(id, name) return destination.removeAttachment(id, name)
...@@ -8617,7 +8701,7 @@ return new Parser; ...@@ -8617,7 +8701,7 @@ return new Parser;
}); });
} }
function propagateAttachmentModification(skip_attachment_dict, function propagateAttachmentModification(context, skip_attachment_dict,
destination, destination,
blob, hash, id, name) { blob, hash, id, name) {
return destination.putAttachment(id, name, blob) return destination.putAttachment(id, name, blob)
...@@ -8632,7 +8716,8 @@ return new Parser; ...@@ -8632,7 +8716,8 @@ return new Parser;
}); });
} }
function checkAndPropagateAttachment(skip_attachment_dict, function checkAndPropagateAttachment(context,
skip_attachment_dict,
status_hash, local_hash, blob, status_hash, local_hash, blob,
source, destination, id, name, source, destination, id, name,
conflict_force, conflict_revert, conflict_force, conflict_revert,
...@@ -8662,7 +8747,7 @@ return new Parser; ...@@ -8662,7 +8747,7 @@ return new Parser;
// Deleted on both side, drop signature // Deleted on both side, drop signature
return context._signature_sub_storage.removeAttachment(id, name) return context._signature_sub_storage.removeAttachment(id, name)
.push(function () { .push(function () {
skip_attachment_dict[id] = null; skip_attachment_dict[name] = null;
}); });
} }
...@@ -8671,7 +8756,7 @@ return new Parser; ...@@ -8671,7 +8756,7 @@ return new Parser;
hash: local_hash hash: local_hash
})) }))
.push(function () { .push(function () {
skip_document_dict[id] = null; skip_attachment_dict[name] = null;
}); });
} }
...@@ -8679,11 +8764,12 @@ return new Parser; ...@@ -8679,11 +8764,12 @@ return new Parser;
// Modified only locally. No conflict or force // Modified only locally. No conflict or force
if (local_hash === null) { if (local_hash === null) {
// Deleted locally // Deleted locally
return propagateAttachmentDeletion(skip_attachment_dict, return propagateAttachmentDeletion(context, skip_attachment_dict,
destination, destination,
id, name); id, name);
} }
return propagateAttachmentModification(skip_attachment_dict, return propagateAttachmentModification(context,
skip_attachment_dict,
destination, blob, destination, blob,
local_hash, id, name); local_hash, id, name);
} }
...@@ -8697,10 +8783,11 @@ return new Parser; ...@@ -8697,10 +8783,11 @@ return new Parser;
// Automatically resolve conflict or force revert // Automatically resolve conflict or force revert
if (remote_hash === null) { if (remote_hash === null) {
// Deleted remotely // Deleted remotely
return propagateAttachmentDeletion(skip_attachment_dict, return propagateAttachmentDeletion(context, skip_attachment_dict,
source, id, name); source, id, name);
} }
return propagateAttachmentModification( return propagateAttachmentModification(
context,
skip_attachment_dict, skip_attachment_dict,
source, source,
remote_blob, remote_blob,
...@@ -8713,7 +8800,8 @@ return new Parser; ...@@ -8713,7 +8800,8 @@ return new Parser;
// Minimize conflict if it can be resolved // Minimize conflict if it can be resolved
if (remote_hash === null) { if (remote_hash === null) {
// Copy remote modification remotely // Copy remote modification remotely
return propagateAttachmentModification(skip_attachment_dict, return propagateAttachmentModification(context,
skip_attachment_dict,
destination, blob, destination, blob,
local_hash, id, name); local_hash, id, name);
} }
...@@ -8724,8 +8812,9 @@ return new Parser; ...@@ -8724,8 +8812,9 @@ return new Parser;
}); });
} }
function checkAttachmentSignatureDifference(skip_attachment_dict, function checkAttachmentSignatureDifference(queue, context,
queue, source, skip_attachment_dict,
source,
destination, id, name, destination, id, name,
conflict_force, conflict_force,
conflict_revert, conflict_revert,
...@@ -8766,7 +8855,8 @@ return new Parser; ...@@ -8766,7 +8855,8 @@ return new Parser;
local_hash = generateHashFromArrayBuffer(array_buffer); local_hash = generateHashFromArrayBuffer(array_buffer);
if (local_hash !== status_hash) { if (local_hash !== status_hash) {
return checkAndPropagateAttachment(skip_attachment_dict, return checkAndPropagateAttachment(context,
skip_attachment_dict,
status_hash, local_hash, blob, status_hash, local_hash, blob,
source, destination, id, name, source, destination, id, name,
conflict_force, conflict_revert, conflict_force, conflict_revert,
...@@ -8775,8 +8865,9 @@ return new Parser; ...@@ -8775,8 +8865,9 @@ return new Parser;
}); });
} }
function checkAttachmentLocalDeletion(skip_attachment_dict, function checkAttachmentLocalDeletion(queue, context,
queue, destination, id, name, source, skip_attachment_dict,
destination, id, name, source,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore) { conflict_ignore) {
var status_hash; var status_hash;
...@@ -8787,7 +8878,8 @@ return new Parser; ...@@ -8787,7 +8878,8 @@ return new Parser;
}) })
.push(function (result) { .push(function (result) {
status_hash = result.hash; status_hash = result.hash;
return checkAndPropagateAttachment(skip_attachment_dict, return checkAndPropagateAttachment(context,
skip_attachment_dict,
status_hash, null, null, status_hash, null, null,
source, destination, id, name, source, destination, id, name,
conflict_force, conflict_revert, conflict_force, conflict_revert,
...@@ -8795,22 +8887,13 @@ return new Parser; ...@@ -8795,22 +8887,13 @@ return new Parser;
}); });
} }
function pushDocumentAttachment(skip_attachment_dict, id, source, function pushDocumentAttachment(context,
destination, options) { skip_attachment_dict, id, source,
var queue = new RSVP.Queue(); destination, signature_allAttachments,
options) {
return queue var local_dict = {},
.push(function () { signature_dict = {};
return RSVP.all([ return source.allAttachments(id)
source.allAttachments(id)
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
}),
context._signature_sub_storage.allAttachments(id)
.push(undefined, function (error) { .push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) && if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) { (error.status_code === 404)) {
...@@ -8818,23 +8901,20 @@ return new Parser; ...@@ -8818,23 +8901,20 @@ return new Parser;
} }
throw error; throw error;
}) })
]); .push(function (source_allAttachments) {
}) var is_modification,
.push(function (result_list) {
var local_dict = {},
signature_dict = {},
is_modification,
is_creation, is_creation,
key; key,
for (key in result_list[0]) { argument_list = [];
if (result_list[0].hasOwnProperty(key)) { for (key in source_allAttachments) {
if (source_allAttachments.hasOwnProperty(key)) {
if (!skip_attachment_dict.hasOwnProperty(key)) { if (!skip_attachment_dict.hasOwnProperty(key)) {
local_dict[key] = null; local_dict[key] = null;
} }
} }
} }
for (key in result_list[1]) { for (key in signature_allAttachments) {
if (result_list[1].hasOwnProperty(key)) { if (signature_allAttachments.hasOwnProperty(key)) {
if (!skip_attachment_dict.hasOwnProperty(key)) { if (!skip_attachment_dict.hasOwnProperty(key)) {
signature_dict[key] = null; signature_dict[key] = null;
} }
...@@ -8848,47 +8928,262 @@ return new Parser; ...@@ -8848,47 +8928,262 @@ return new Parser;
is_creation = !signature_dict.hasOwnProperty(key) is_creation = !signature_dict.hasOwnProperty(key)
&& options.check_creation; && options.check_creation;
if (is_modification === true || is_creation === true) { if (is_modification === true || is_creation === true) {
checkAttachmentSignatureDifference(skip_attachment_dict, argument_list.push([undefined,
queue, source, context,
skip_attachment_dict,
source,
destination, id, key, destination, id, key,
options.conflict_force, options.conflict_force,
options.conflict_revert, options.conflict_revert,
options.conflict_ignore, options.conflict_ignore,
is_creation, is_creation,
is_modification); is_modification]);
} }
} }
} }
return dispatchQueue(
context,
checkAttachmentSignatureDifference,
argument_list,
context._parallel_operation_attachment_amount
);
})
.push(function () {
var key, argument_list = [];
if (options.check_deletion === true) { if (options.check_deletion === true) {
for (key in signature_dict) { for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) { if (signature_dict.hasOwnProperty(key)) {
if (!local_dict.hasOwnProperty(key)) { if (!local_dict.hasOwnProperty(key)) {
checkAttachmentLocalDeletion(skip_attachment_dict, argument_list.push([undefined,
queue, destination, id, key, context,
skip_attachment_dict,
destination, id, key,
source, source,
options.conflict_force, options.conflict_force,
options.conflict_revert, options.conflict_revert,
options.conflict_ignore); options.conflict_ignore]);
}
}
}
return dispatchQueue(
context,
checkAttachmentLocalDeletion,
argument_list,
context._parallel_operation_attachment_amount
);
}
});
}
function propagateFastAttachmentDeletion(queue, id, name, storage) {
return queue
.push(function () {
return storage.removeAttachment(id, name);
});
}
function propagateFastAttachmentModification(queue, id, key, source,
destination, signature, hash) {
return queue
.push(function () {
return signature.getAttachment(id, key, {format: 'json'})
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {hash: null};
}
throw error;
})
.push(function (result) {
if (result.hash !== hash) {
return source.getAttachment(id, key)
.push(function (blob) {
return destination.putAttachment(id, key, blob);
})
.push(function () {
return signature.putAttachment(id, key, JSON.stringify({
hash: hash
}));
});
}
});
});
}
function repairFastDocumentAttachment(context, id,
signature_hash,
signature_attachment_hash,
signature_from_local) {
if (signature_hash === signature_attachment_hash) {
// No replication to do
return;
}
return new RSVP.Queue()
.push(function () {
return RSVP.all([
context._signature_sub_storage.allAttachments(id),
context._local_sub_storage.allAttachments(id),
context._remote_sub_storage.allAttachments(id)
]);
})
.push(function (result_list) {
var key,
source_attachment_dict,
destination_attachment_dict,
source,
destination,
push_argument_list = [],
delete_argument_list = [],
signature_attachment_dict = result_list[0],
local_attachment_dict = result_list[1],
remote_attachment_list = result_list[2],
check_local_modification =
context._check_local_attachment_modification,
check_local_creation = context._check_local_attachment_creation,
check_local_deletion = context._check_local_attachment_deletion,
check_remote_modification =
context._check_remote_attachment_modification,
check_remote_creation = context._check_remote_attachment_creation,
check_remote_deletion = context._check_remote_attachment_deletion;
if (signature_from_local) {
source_attachment_dict = local_attachment_dict;
destination_attachment_dict = remote_attachment_list;
source = context._local_sub_storage;
destination = context._remote_sub_storage;
} else {
source_attachment_dict = remote_attachment_list;
destination_attachment_dict = local_attachment_dict;
source = context._remote_sub_storage;
destination = context._local_sub_storage;
check_local_modification = check_remote_modification;
check_local_creation = check_remote_creation;
check_local_deletion = check_remote_deletion;
check_remote_creation = check_local_creation;
check_remote_deletion = check_local_deletion;
}
// Push all source attachments
for (key in source_attachment_dict) {
if (source_attachment_dict.hasOwnProperty(key)) {
if ((check_local_creation &&
!signature_attachment_dict.hasOwnProperty(key)) ||
(check_local_modification &&
signature_attachment_dict.hasOwnProperty(key))) {
push_argument_list.push([
undefined,
id,
key,
source,
destination,
context._signature_sub_storage,
signature_hash
]);
}
}
}
// Delete remaining signature + remote attachments
for (key in signature_attachment_dict) {
if (signature_attachment_dict.hasOwnProperty(key)) {
if (check_local_deletion &&
!source_attachment_dict.hasOwnProperty(key)) {
delete_argument_list.push([
undefined,
id,
key,
context._signature_sub_storage
]);
}
}
}
for (key in destination_attachment_dict) {
if (destination_attachment_dict.hasOwnProperty(key)) {
if (!source_attachment_dict.hasOwnProperty(key)) {
if ((check_local_deletion &&
signature_attachment_dict.hasOwnProperty(key)) ||
(check_remote_creation &&
!signature_attachment_dict.hasOwnProperty(key))) {
delete_argument_list.push([
undefined,
id,
key,
destination
]);
} }
} }
} }
} }
return RSVP.all([
dispatchQueue(
context,
propagateFastAttachmentModification,
push_argument_list,
context._parallel_operation_attachment_amount
),
dispatchQueue(
context,
propagateFastAttachmentDeletion,
delete_argument_list,
context._parallel_operation_attachment_amount
)
]);
})
.push(function () {
// Mark that all attachments have been synchronized
return context._signature_sub_storage.put(id, {
hash: signature_hash,
attachment_hash: signature_hash,
from_local: signature_from_local
});
}); });
} }
function repairDocumentAttachment(context, id, signature_hash_key,
signature_hash,
signature_attachment_hash,
signature_from_local) {
if (signature_hash_key !== undefined) {
return repairFastDocumentAttachment(context, id,
signature_hash,
signature_attachment_hash,
signature_from_local);
}
function repairDocumentAttachment(id) {
var skip_attachment_dict = {}; var skip_attachment_dict = {};
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
if (context._check_local_attachment_modification ||
context._check_local_attachment_creation ||
context._check_local_attachment_deletion ||
context._check_remote_attachment_modification ||
context._check_remote_attachment_creation ||
context._check_remote_attachment_deletion) {
return context._signature_sub_storage.allAttachments(id);
}
return {};
})
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
})
.push(function (signature_allAttachments) {
if (context._check_local_attachment_modification || if (context._check_local_attachment_modification ||
context._check_local_attachment_creation || context._check_local_attachment_creation ||
context._check_local_attachment_deletion) { context._check_local_attachment_deletion) {
return pushDocumentAttachment( return pushDocumentAttachment(
context,
skip_attachment_dict, skip_attachment_dict,
id, id,
context._local_sub_storage, context._local_sub_storage,
context._remote_sub_storage, context._remote_sub_storage,
signature_allAttachments,
{ {
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL), CONFLICT_KEEP_LOCAL),
...@@ -8901,18 +9196,24 @@ return new Parser; ...@@ -8901,18 +9196,24 @@ return new Parser;
check_creation: context._check_local_attachment_creation, check_creation: context._check_local_attachment_creation,
check_deletion: context._check_local_attachment_deletion check_deletion: context._check_local_attachment_deletion
} }
); )
.push(function () {
return signature_allAttachments;
});
} }
return signature_allAttachments;
}) })
.push(function () { .push(function (signature_allAttachments) {
if (context._check_remote_attachment_modification || if (context._check_remote_attachment_modification ||
context._check_remote_attachment_creation || context._check_remote_attachment_creation ||
context._check_remote_attachment_deletion) { context._check_remote_attachment_deletion) {
return pushDocumentAttachment( return pushDocumentAttachment(
context,
skip_attachment_dict, skip_attachment_dict,
id, id,
context._remote_sub_storage, context._remote_sub_storage,
context._local_sub_storage, context._local_sub_storage,
signature_allAttachments,
{ {
use_revert_post: context._use_remote_post, use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
...@@ -8931,16 +9232,39 @@ return new Parser; ...@@ -8931,16 +9232,39 @@ return new Parser;
}); });
} }
function propagateModification(source, destination, doc, hash, id, function propagateModification(context, source, destination, doc, hash, id,
skip_document_dict,
skip_deleted_document_dict,
options) { options) {
var result, var result = new RSVP.Queue(),
post_id, post_id,
to_skip = true; to_skip = true,
from_local;
if (options === undefined) { if (options === undefined) {
options = {}; options = {};
} }
from_local = options.from_local;
if (doc === null) {
result
.push(function () {
return source.get(id);
})
.push(function (source_doc) {
doc = source_doc;
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
throw new SkipError(id);
}
throw error;
});
}
if (options.use_post) { if (options.use_post) {
result = destination.post(doc) result
.push(function () {
return destination.post(doc);
})
.push(function (new_id) { .push(function (new_id) {
to_skip = false; to_skip = false;
post_id = new_id; post_id = new_id;
...@@ -8982,17 +9306,30 @@ return new Parser; ...@@ -8982,17 +9306,30 @@ return new Parser;
.push(function () { .push(function () {
to_skip = true; to_skip = true;
return context._signature_sub_storage.put(post_id, { return context._signature_sub_storage.put(post_id, {
"hash": hash hash: hash,
from_local: from_local
}); });
}) })
.push(function () { .push(function () {
skip_document_dict[post_id] = null; skip_document_dict[post_id] = null;
}); });
} else { } else {
result = destination.put(id, doc) result
.push(function () {
// Drop signature if the destination document was empty
// but a signature exists
if (options.create_new_document === true) {
delete skip_deleted_document_dict[id];
return context._signature_sub_storage.remove(id);
}
})
.push(function () {
return destination.put(id, doc);
})
.push(function () { .push(function () {
return context._signature_sub_storage.put(id, { return context._signature_sub_storage.put(id, {
"hash": hash hash: hash,
from_local: from_local
}); });
}); });
} }
...@@ -9001,15 +9338,29 @@ return new Parser; ...@@ -9001,15 +9338,29 @@ return new Parser;
if (to_skip) { if (to_skip) {
skip_document_dict[id] = null; skip_document_dict[id] = null;
} }
})
.push(undefined, function (error) {
if (error instanceof SkipError) {
return;
}
throw error;
}); });
} }
function propagateDeletion(destination, id) { function propagateDeletion(context, destination, id, skip_document_dict,
skip_deleted_document_dict) {
// Do not delete a document if it has an attachment // Do not delete a document if it has an attachment
// ie, replication should prevent losing user data // ie, replication should prevent losing user data
// Synchronize attachments before, to ensure // Synchronize attachments before, to ensure
// all of them will be deleted too // all of them will be deleted too
return repairDocumentAttachment(id) var result;
if (context._signature_hash_key !== undefined) {
result = destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
});
} else {
result = repairDocumentAttachment(context, id)
.push(function () { .push(function () {
return destination.allAttachments(id); return destination.allAttachments(id);
}) })
...@@ -9026,17 +9377,37 @@ return new Parser; ...@@ -9026,17 +9377,37 @@ return new Parser;
return; return;
} }
throw error; throw error;
}) });
}
return result
.push(function () { .push(function () {
skip_document_dict[id] = null; skip_document_dict[id] = null;
// No need to sync attachment twice on this document
skip_deleted_document_dict[id] = null;
}); });
} }
function checkAndPropagate(status_hash, local_hash, doc, function checkAndPropagate(context, skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
status_hash, local_hash, doc,
source, destination, id, source, destination, id,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore, conflict_ignore,
options) { options) {
var from_local = options.from_local;
return new RSVP.Queue()
.push(function () {
if (options.signature_hash_key !== undefined) {
return callAllDocsOnStorage(context, destination,
cache, destination_key)
.push(function (result) {
if (result.hasOwnProperty(id)) {
return [null, result[id][options.signature_hash_key]];
}
return [null, null];
});
}
return destination.get(id) return destination.get(id)
.push(function (remote_doc) { .push(function (remote_doc) {
return [remote_doc, generateHash(stringify(remote_doc))]; return [remote_doc, generateHash(stringify(remote_doc))];
...@@ -9046,11 +9417,12 @@ return new Parser; ...@@ -9046,11 +9417,12 @@ return new Parser;
return [null, null]; return [null, null];
} }
throw error; throw error;
});
}) })
.push(function (remote_list) { .push(function (remote_list) {
var remote_doc = remote_list[0], var remote_doc = remote_list[0],
remote_hash = remote_list[1]; remote_hash = remote_list[1];
if (local_hash === remote_hash) { if (local_hash === remote_hash) {
// Same modifications on both side // Same modifications on both side
if (local_hash === null) { if (local_hash === null) {
...@@ -9062,7 +9434,8 @@ return new Parser; ...@@ -9062,7 +9434,8 @@ return new Parser;
} }
return context._signature_sub_storage.put(id, { return context._signature_sub_storage.put(id, {
"hash": local_hash hash: local_hash,
from_local: from_local
}) })
.push(function () { .push(function () {
skip_document_dict[id] = null; skip_document_dict[id] = null;
...@@ -9073,12 +9446,20 @@ return new Parser; ...@@ -9073,12 +9446,20 @@ return new Parser;
// Modified only locally. No conflict or force // Modified only locally. No conflict or force
if (local_hash === null) { if (local_hash === null) {
// Deleted locally // Deleted locally
return propagateDeletion(destination, id); return propagateDeletion(context, destination, id,
skip_document_dict,
skip_deleted_document_dict);
} }
return propagateModification(source, destination, doc, return propagateModification(context, source, destination, doc,
local_hash, id, local_hash, id, skip_document_dict,
skip_deleted_document_dict,
{use_post: ((options.use_post) && {use_post: ((options.use_post) &&
(remote_hash === null))}); (remote_hash === null)),
from_local: from_local,
create_new_document:
((remote_hash === null) &&
(status_hash !== null))
});
} }
// Conflict cases // Conflict cases
...@@ -9090,34 +9471,50 @@ return new Parser; ...@@ -9090,34 +9471,50 @@ return new Parser;
// Automatically resolve conflict or force revert // Automatically resolve conflict or force revert
if (remote_hash === null) { if (remote_hash === null) {
// Deleted remotely // Deleted remotely
return propagateDeletion(source, id); return propagateDeletion(context, source, id, skip_document_dict,
skip_deleted_document_dict);
} }
return propagateModification( return propagateModification(
context,
destination, destination,
source, source,
remote_doc, remote_doc,
remote_hash, remote_hash,
id, id,
skip_document_dict,
skip_deleted_document_dict,
{use_post: ((options.use_revert_post) && {use_post: ((options.use_revert_post) &&
(local_hash === null))} (local_hash === null)),
from_local: !from_local,
create_new_document: ((local_hash === null) &&
(status_hash !== null))}
); );
} }
// Minimize conflict if it can be resolved // Minimize conflict if it can be resolved
if (remote_hash === null) { if (remote_hash === null) {
// Copy remote modification remotely // Copy remote modification remotely
return propagateModification(source, destination, doc, return propagateModification(context, source, destination, doc,
local_hash, id, local_hash, id, skip_document_dict,
{use_post: options.use_post}); skip_deleted_document_dict,
} {use_post: options.use_post,
from_local: from_local,
create_new_document:
(status_hash !== null)});
}
doc = doc || local_hash;
remote_doc = remote_doc || remote_hash;
throw new jIO.util.jIOError("Conflict on '" + id + "': " + throw new jIO.util.jIOError("Conflict on '" + id + "': " +
stringify(doc || '') + " !== " + stringify(doc) + " !== " +
stringify(remote_doc || ''), stringify(remote_doc),
409); 409);
}); });
} }
function checkLocalDeletion(queue, destination, id, source, function checkLocalDeletion(queue, context, skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
destination, id, source,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore, options) { conflict_ignore, options) {
var status_hash; var status_hash;
...@@ -9126,124 +9523,99 @@ return new Parser; ...@@ -9126,124 +9523,99 @@ return new Parser;
return context._signature_sub_storage.get(id); return context._signature_sub_storage.get(id);
}) })
.push(function (result) { .push(function (result) {
status_hash = result.hash; status_hash = result.hash;
return checkAndPropagate(status_hash, null, null, return checkAndPropagate(context, skip_document_dict,
source, destination, id, skip_deleted_document_dict,
conflict_force, conflict_revert, cache, destination_key,
conflict_ignore, status_hash, null, null,
options);
});
}
function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
is_creation, is_modification,
getMethod, options) {
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([
getMethod(id),
{hash: null}
]);
}
if (is_modification === true) {
return RSVP.all([
getMethod(id),
context._signature_sub_storage.get(id)
]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkSignatureDifference",
409);
})
.push(function (result_list) {
var doc = result_list[0],
local_hash = generateHash(stringify(doc)),
status_hash = result_list[1].hash;
if (local_hash !== status_hash) {
return checkAndPropagate(status_hash, local_hash, doc,
source, destination, id, source, destination, id,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore, conflict_ignore,
options); options);
}
}); });
} }
function checkBulkSignatureDifference(queue, source, destination, id_list, function checkSignatureDifference(queue, context, skip_document_dict,
document_status_list, options, skip_deleted_document_dict,
cache, destination_key,
source, destination, id,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore) { conflict_ignore,
local_hash, status_hash,
options) {
queue queue
.push(function () { .push(function () {
return source.bulk(id_list); if (local_hash === null) {
}) // Hash was not provided by the allDocs query
.push(function (result_list) { return source.get(id);
var i,
sub_queue = new RSVP.Queue();
function getResult(j) {
return function (id) {
if (id !== id_list[j].parameter_list[0]) {
throw new Error("Does not access expected ID " + id);
} }
return result_list[j]; return null;
}; })
.push(function (doc) {
if (local_hash === null) {
// Hash was not provided by the allDocs query
local_hash = generateHash(stringify(doc));
} }
for (i = 0; i < result_list.length; i += 1) { if (local_hash !== status_hash) {
checkSignatureDifference(sub_queue, source, destination, return checkAndPropagate(context, skip_document_dict,
id_list[i].parameter_list[0], skip_deleted_document_dict,
cache, destination_key,
status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore, conflict_ignore,
document_status_list[i].is_creation, options);
document_status_list[i].is_modification,
getResult(i), options);
} }
return sub_queue;
}); });
} }
function pushStorage(source, destination, options) { function pushStorage(context, skip_document_dict,
var queue = new RSVP.Queue(); skip_deleted_document_dict,
cache, source_key, destination_key,
source, destination, signature_allDocs, options) {
var argument_list = [],
argument_list_deletion = [];
if (!options.hasOwnProperty("use_post")) { if (!options.hasOwnProperty("use_post")) {
options.use_post = false; options.use_post = false;
} }
if (!options.hasOwnProperty("use_revert_post")) { if (!options.hasOwnProperty("use_revert_post")) {
options.use_revert_post = false; options.use_revert_post = false;
} }
return queue return callAllDocsOnStorage(context, source, cache, source_key)
.push(function () { .push(function (source_allDocs) {
return RSVP.all([
source.allDocs(context._query_options),
context._signature_sub_storage.allDocs()
]);
})
.push(function (result_list) {
var i, var i,
local_dict = {}, local_dict = {},
document_list = [],
document_status_list = [],
signature_dict = {}, signature_dict = {},
is_modification, is_modification,
is_creation, is_creation,
key; status_hash,
for (i = 0; i < result_list[0].data.total_rows; i += 1) { local_hash,
key,
queue = new RSVP.Queue();
for (key in source_allDocs) {
if (source_allDocs.hasOwnProperty(key)) {
if (!skip_document_dict.hasOwnProperty(key)) {
local_dict[key] = source_allDocs[key];
}
}
}
/*
for (i = 0; i < source_allDocs.data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty( if (!skip_document_dict.hasOwnProperty(
result_list[0].data.rows[i].id source_allDocs.data.rows[i].id
)) { )) {
local_dict[result_list[0].data.rows[i].id] = i; local_dict[source_allDocs.data.rows[i].id] =
source_allDocs.data.rows[i].value;
} }
} }
for (i = 0; i < result_list[1].data.total_rows; i += 1) { */
for (i = 0; i < signature_allDocs.data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty( if (!skip_document_dict.hasOwnProperty(
result_list[1].data.rows[i].id signature_allDocs.data.rows[i].id
)) { )) {
signature_dict[result_list[1].data.rows[i].id] = i; signature_dict[signature_allDocs.data.rows[i].id] =
signature_allDocs.data.rows[i].value.hash;
} }
} }
for (key in local_dict) { for (key in local_dict) {
...@@ -9252,63 +9624,120 @@ return new Parser; ...@@ -9252,63 +9624,120 @@ return new Parser;
&& options.check_modification; && options.check_modification;
is_creation = !signature_dict.hasOwnProperty(key) is_creation = !signature_dict.hasOwnProperty(key)
&& options.check_creation; && options.check_creation;
if (is_creation === true) {
status_hash = null;
} else if (is_modification === true) {
status_hash = signature_dict[key];
}
local_hash = null;
if (options.signature_hash_key !== undefined) {
local_hash = local_dict[key][options.signature_hash_key];
if (is_modification === true) {
// Bypass fetching all documents and calculating the sha
// Compare the select list values returned by allDocs calls
is_modification = false;
if (local_hash !== status_hash) {
is_modification = true;
}
}
}
if (is_modification === true || is_creation === true) { if (is_modification === true || is_creation === true) {
if (options.use_bulk_get === true) { argument_list.push([undefined, context, skip_document_dict,
document_list.push({ skip_deleted_document_dict,
method: "get", cache, destination_key,
parameter_list: [key] source, destination,
}); key,
document_status_list.push({
is_creation: is_creation,
is_modification: is_modification
});
} else {
checkSignatureDifference(queue, source, destination, key,
options.conflict_force, options.conflict_force,
options.conflict_revert, options.conflict_revert,
options.conflict_ignore, options.conflict_ignore,
is_creation, is_modification, local_hash, status_hash,
source.get.bind(source), options]);
options);
} }
} }
} }
} queue
if (options.check_deletion === true) { .push(function () {
return dispatchQueue(
context,
checkSignatureDifference,
argument_list,
options.operation_amount
);
});
for (key in signature_dict) { for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) { if (signature_dict.hasOwnProperty(key)) {
if (!local_dict.hasOwnProperty(key)) { if (!local_dict.hasOwnProperty(key)) {
checkLocalDeletion(queue, destination, key, source, if (options.check_deletion === true) {
argument_list_deletion.push([undefined,
context,
skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
destination, key,
source,
options.conflict_force, options.conflict_force,
options.conflict_revert, options.conflict_revert,
options.conflict_ignore, options.conflict_ignore,
options); options]);
} else {
skip_deleted_document_dict[key] = null;
} }
} }
} }
} }
if ((options.use_bulk_get === true) && (document_list.length !== 0)) { if (argument_list_deletion.length !== 0) {
checkBulkSignatureDifference(queue, source, destination, queue.push(function () {
document_list, document_status_list, return dispatchQueue(
options, context,
options.conflict_force, checkLocalDeletion,
options.conflict_revert, argument_list_deletion,
options.conflict_ignore); options.operation_amount
);
});
}
return queue;
});
} }
function repairDocument(queue, context, id, signature_hash_key,
signature_hash, signature_attachment_hash,
signature_from_local) {
queue.push(function () {
return repairDocumentAttachment(context, id, signature_hash_key,
signature_hash,
signature_attachment_hash,
signature_from_local);
}); });
} }
ReplicateStorage.prototype.repair = function () {
var context = this,
argument_list = arguments,
skip_document_dict = {},
skip_deleted_document_dict = {},
cache = {};
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
// Ensure that the document storage is usable // Ensure that the document storage is usable
return context._signature_sub_storage.__storage._sub_storage.get( if (context._custom_signature_sub_storage === false) {
// Do not sync the signature document
skip_document_dict[context._signature_hash] = null;
return context._signature_sub_storage.__storage._sub_storage
.__storage._sub_storage.get(
context._signature_hash context._signature_hash
); );
}
}) })
.push(undefined, function (error) { .push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) && if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) { (error.status_code === 404)) {
return context._signature_sub_storage.__storage._sub_storage.put( return context._signature_sub_storage.__storage._sub_storage
.__storage._sub_storage.put(
context._signature_hash, context._signature_hash,
{} {}
); );
...@@ -9335,11 +9764,28 @@ return new Parser; ...@@ -9335,11 +9764,28 @@ return new Parser;
}) })
.push(function () { .push(function () {
if (context._check_local_modification ||
context._check_local_creation ||
context._check_local_deletion ||
context._check_remote_modification ||
context._check_remote_creation ||
context._check_remote_deletion) {
return context._signature_sub_storage.allDocs({
select_list: ['hash']
});
}
})
.push(function (signature_allDocs) {
if (context._check_local_modification || if (context._check_local_modification ||
context._check_local_creation || context._check_local_creation ||
context._check_local_deletion) { context._check_local_deletion) {
return pushStorage(context._local_sub_storage, return pushStorage(context, skip_document_dict,
skip_deleted_document_dict,
cache, 'local', 'remote',
context._local_sub_storage,
context._remote_sub_storage, context._remote_sub_storage,
signature_allDocs,
{ {
use_post: context._use_remote_post, use_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
...@@ -9350,28 +9796,27 @@ return new Parser; ...@@ -9350,28 +9796,27 @@ return new Parser;
CONFLICT_CONTINUE), CONFLICT_CONTINUE),
check_modification: context._check_local_modification, check_modification: context._check_local_modification,
check_creation: context._check_local_creation, check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion check_deletion: context._check_local_deletion,
}); operation_amount: context._parallel_operation_amount,
} signature_hash_key: context._signature_hash_key,
from_local: true
}) })
.push(function () { .push(function () {
// Autoactivate bulk if substorage implements it return signature_allDocs;
// Keep it like this until the bulk API is stabilized });
var use_bulk_get = false;
try {
use_bulk_get = context._remote_sub_storage.hasCapacity("bulk_get");
} catch (error) {
if (!((error instanceof jIO.util.jIOError) &&
(error.status_code === 501))) {
throw error;
}
} }
return signature_allDocs;
})
.push(function (signature_allDocs) {
if (context._check_remote_modification || if (context._check_remote_modification ||
context._check_remote_creation || context._check_remote_creation ||
context._check_remote_deletion) { context._check_remote_deletion) {
return pushStorage(context._remote_sub_storage, return pushStorage(context, skip_document_dict,
context._local_sub_storage, { skip_deleted_document_dict,
use_bulk_get: use_bulk_get, cache, 'remote', 'local',
context._remote_sub_storage,
context._local_sub_storage,
signature_allDocs, {
use_revert_post: context._use_remote_post, use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE), CONFLICT_KEEP_REMOTE),
...@@ -9381,7 +9826,10 @@ return new Parser; ...@@ -9381,7 +9826,10 @@ return new Parser;
CONFLICT_CONTINUE), CONFLICT_CONTINUE),
check_modification: context._check_remote_modification, check_modification: context._check_remote_modification,
check_creation: context._check_remote_creation, check_creation: context._check_remote_creation,
check_deletion: context._check_remote_deletion check_deletion: context._check_remote_deletion,
operation_amount: context._parallel_operation_amount,
signature_hash_key: context._signature_hash_key,
from_local: false
}); });
} }
}) })
...@@ -9394,22 +9842,33 @@ return new Parser; ...@@ -9394,22 +9842,33 @@ return new Parser;
context._check_remote_attachment_deletion) { context._check_remote_attachment_deletion) {
// Attachments are synchronized if and only if their parent document // Attachments are synchronized if and only if their parent document
// has been also marked as synchronized. // has been also marked as synchronized.
return context._signature_sub_storage.allDocs() return context._signature_sub_storage.allDocs({
select_list: ['hash', 'attachment_hash', 'from_local']
})
.push(function (result) { .push(function (result) {
var i, var i,
repair_document_queue = new RSVP.Queue(); local_argument_list = [],
row,
len = result.data.total_rows;
function repairDocument(id) { for (i = 0; i < len; i += 1) {
repair_document_queue row = result.data.rows[i];
.push(function () { // Do not synchronize attachment if one version of the document
return repairDocumentAttachment(id); // is deleted but not pushed to the other storage
}); if (!skip_deleted_document_dict.hasOwnProperty(row.id)) {
local_argument_list.push(
[undefined, context, row.id, context._signature_hash_key,
row.value.hash, row.value.attachment_hash,
row.value.from_local]
);
} }
for (i = 0; i < result.data.total_rows; i += 1) {
repairDocument(result.data.rows[i].id);
} }
return repair_document_queue; return dispatchQueue(
context,
repairDocument,
local_argument_list,
context._parallel_operation_amount
);
}); });
} }
}); });
...@@ -11070,56 +11529,6 @@ return new Parser; ...@@ -11070,56 +11529,6 @@ return new Parser;
}); });
}; };
ERP5Storage.prototype.bulk = function (request_list) {
var i,
storage = this,
bulk_list = [];
for (i = 0; i < request_list.length; i += 1) {
if (request_list[i].method !== "get") {
throw new Error("ERP5Storage: not supported " +
request_list[i].method + " in bulk");
}
bulk_list.push({
relative_url: request_list[i].parameter_list[0],
view: storage._default_view_reference
});
}
return getSiteDocument(storage)
.push(function (site_hal) {
var form_data = new FormData();
form_data.append("bulk_list", JSON.stringify(bulk_list));
return jIO.util.ajax({
"type": "POST",
"url": site_hal._actions.bulk.href,
"data": form_data,
// "headers": {
// "Content-Type": "application/json"
// },
"xhrFields": {
withCredentials: true
}
});
})
.push(function (response) {
var result_list = [],
hateoas = JSON.parse(response.target.responseText);
function pushResult(json) {
return extractPropertyFromFormJSON(json)
.push(function (json2) {
return convertJSONToGet(json2);
});
}
for (i = 0; i < hateoas.result_list.length; i += 1) {
result_list.push(pushResult(hateoas.result_list[i]));
}
return RSVP.all(result_list);
});
};
ERP5Storage.prototype.post = function (data) { ERP5Storage.prototype.post = function (data) {
var context = this, var context = this,
new_id; new_id;
...@@ -11335,6 +11744,7 @@ return new Parser; ...@@ -11335,6 +11744,7 @@ return new Parser;
"type": "POST", "type": "POST",
"url": name, "url": name,
"data": data, "data": data,
"dataType": "blob",
"xhrFields": { "xhrFields": {
withCredentials: true withCredentials: true
} }
...@@ -11345,7 +11755,7 @@ return new Parser; ...@@ -11345,7 +11755,7 @@ return new Parser;
ERP5Storage.prototype.hasCapacity = function (name) { ERP5Storage.prototype.hasCapacity = function (name) {
return ((name === "list") || (name === "query") || return ((name === "list") || (name === "query") ||
(name === "select") || (name === "limit") || (name === "select") || (name === "limit") ||
(name === "sort")) || (name === "bulk_get"); (name === "sort"));
}; };
function isSingleLocalRoles(parsed_query) { function isSingleLocalRoles(parsed_query) {
...@@ -12442,9 +12852,7 @@ return new Parser; ...@@ -12442,9 +12852,7 @@ return new Parser;
return tx; return tx;
} }
function handleCursor(request, callback) { function handleCursor(request, callback, resolve, reject) {
function resolver(resolve, reject) {
// Open DB //
request.onerror = function (error) { request.onerror = function (error) {
if (request.transaction) { if (request.transaction) {
request.transaction.abort(); request.transaction.abort();
...@@ -12469,9 +12877,6 @@ return new Parser; ...@@ -12469,9 +12877,6 @@ return new Parser;
} }
}; };
} }
// XXX Canceller???
return new RSVP.Promise(resolver);
}
IndexedDBStorage.prototype.buildQuery = function (options) { IndexedDBStorage.prototype.buildQuery = function (options) {
var result_list = []; var result_list = [];
...@@ -12492,40 +12897,50 @@ return new Parser; ...@@ -12492,40 +12897,50 @@ return new Parser;
} }
return openIndexedDB(this) return openIndexedDB(this)
.push(function (db) { .push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var tx = openTransaction(db, ["metadata"], "readonly"); var tx = openTransaction(db, ["metadata"], "readonly");
if (options.include_docs === true) { if (options.include_docs === true) {
return handleCursor(tx.objectStore("metadata").index("_id") handleCursor(tx.objectStore("metadata").index("_id").openCursor(),
.openCursor(), pushIncludedMetadata); pushIncludedMetadata, resolve, reject);
} else {
handleCursor(tx.objectStore("metadata").index("_id")
.openKeyCursor(), pushMetadata, resolve, reject);
} }
return handleCursor(tx.objectStore("metadata").index("_id") });
.openKeyCursor(), pushMetadata);
}) })
.push(function () { .push(function () {
return result_list; return result_list;
}); });
}; };
function handleGet(request) { function handleGet(store, id, resolve, reject) {
function resolver(resolve, reject) { var request = store.get(id);
request.onerror = reject; request.onerror = reject;
request.onsuccess = function () { request.onsuccess = function () {
if (request.result) { if (request.result) {
resolve(request.result); resolve(request.result);
} else {
reject(new jIO.util.jIOError(
"IndexedDB: cannot find object '" + id + "' in the '" +
store.name + "' store",
404
));
} }
// XXX How to get ID
reject(new jIO.util.jIOError("Cannot find document", 404));
}; };
} }
return new RSVP.Promise(resolver);
}
IndexedDBStorage.prototype.get = function (id) { IndexedDBStorage.prototype.get = function (id) {
return openIndexedDB(this) return openIndexedDB(this)
.push(function (db) { .push(function (db) {
var transaction = openTransaction(db, ["metadata"], return new RSVP.Promise(function (resolve, reject) {
"readonly"); var transaction = openTransaction(db, ["metadata"], "readonly");
return handleGet(transaction.objectStore("metadata").get(id)); handleGet(
transaction.objectStore("metadata"),
id,
resolve,
reject
);
});
}) })
.push(function (result) { .push(function (result) {
return result.doc; return result.doc;
...@@ -12541,37 +12956,52 @@ return new Parser; ...@@ -12541,37 +12956,52 @@ return new Parser;
return openIndexedDB(this) return openIndexedDB(this)
.push(function (db) { .push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var transaction = openTransaction(db, ["metadata", "attachment"], var transaction = openTransaction(db, ["metadata", "attachment"],
"readonly"); "readonly");
return RSVP.all([ function getAttachments() {
handleGet(transaction.objectStore("metadata").get(id)), handleCursor(
handleCursor(transaction.objectStore("attachment").index("_id") transaction.objectStore("attachment").index("_id")
.openCursor(IDBKeyRange.only(id)), addEntry) .openCursor(IDBKeyRange.only(id)),
]); addEntry,
resolve,
reject
);
}
handleGet(
transaction.objectStore("metadata"),
id,
getAttachments,
reject
);
});
}) })
.push(function () { .push(function () {
return attachment_dict; return attachment_dict;
}); });
}; };
function handleRequest(request) { function handleRequest(request, resolve, reject) {
function resolver(resolve, reject) {
request.onerror = reject; request.onerror = reject;
request.onsuccess = function () { request.onsuccess = function () {
resolve(request.result); resolve(request.result);
}; };
} }
return new RSVP.Promise(resolver);
}
IndexedDBStorage.prototype.put = function (id, metadata) { IndexedDBStorage.prototype.put = function (id, metadata) {
return openIndexedDB(this) return openIndexedDB(this)
.push(function (db) { .push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var transaction = openTransaction(db, ["metadata"], "readwrite"); var transaction = openTransaction(db, ["metadata"], "readwrite");
return handleRequest(transaction.objectStore("metadata").put({ handleRequest(
transaction.objectStore("metadata").put({
"_id": id, "_id": id,
"doc": metadata "doc": metadata
})); }),
resolve,
reject
);
});
}); });
}; };
...@@ -12580,19 +13010,38 @@ return new Parser; ...@@ -12580,19 +13010,38 @@ return new Parser;
} }
IndexedDBStorage.prototype.remove = function (id) { IndexedDBStorage.prototype.remove = function (id) {
var resolved_amount = 0;
return openIndexedDB(this) return openIndexedDB(this)
.push(function (db) { .push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
function resolver() {
if (resolved_amount < 2) {
resolved_amount += 1;
} else {
resolve();
}
}
var transaction = openTransaction(db, ["metadata", "attachment", var transaction = openTransaction(db, ["metadata", "attachment",
"blob"], "readwrite"); "blob"], "readwrite");
return RSVP.all([ handleRequest(
handleRequest(transaction transaction.objectStore("metadata")["delete"](id),
.objectStore("metadata")["delete"](id)), resolver,
reject
);
// XXX Why not possible to delete with KeyCursor? // XXX Why not possible to delete with KeyCursor?
handleCursor(transaction.objectStore("attachment").index("_id") handleCursor(transaction.objectStore("attachment").index("_id")
.openCursor(IDBKeyRange.only(id)), deleteEntry), .openCursor(IDBKeyRange.only(id)),
deleteEntry,
resolver,
reject
);
handleCursor(transaction.objectStore("blob").index("_id") handleCursor(transaction.objectStore("blob").index("_id")
.openCursor(IDBKeyRange.only(id)), deleteEntry) .openCursor(IDBKeyRange.only(id)),
]); deleteEntry,
resolver,
reject
);
});
}); });
}; };
...@@ -12606,48 +13055,65 @@ return new Parser; ...@@ -12606,48 +13055,65 @@ return new Parser;
} }
return openIndexedDB(this) return openIndexedDB(this)
.push(function (db) { .push(function (db) {
transaction = openTransaction(db, ["attachment", "blob"], "readonly"); return new RSVP.Promise(function (resolve, reject) {
// XXX Should raise if key is not good transaction = openTransaction(
return handleGet(transaction.objectStore("attachment") db,
.get(buildKeyPath([id, name]))); ["attachment", "blob"],
}) "readonly"
.push(function (attachment) { );
function getBlob(attachment) {
var total_length = attachment.info.length, var total_length = attachment.info.length,
i, result_list = [],
promise_list = [],
store = transaction.objectStore("blob"), store = transaction.objectStore("blob"),
start_index, start_index,
end_index; end_index;
type = attachment.info.content_type; type = attachment.info.content_type;
start = options.start || 0; start = options.start || 0;
end = options.end || total_length; end = options.end || total_length;
if (end > total_length) { if (end > total_length) {
end = total_length; end = total_length;
} }
if (start < 0 || end < 0) { if (start < 0 || end < 0) {
throw new jIO.util.jIOError("_start and _end must be positive", throw new jIO.util.jIOError(
400); "_start and _end must be positive",
400
);
} }
if (start > end) { if (start > end) {
throw new jIO.util.jIOError("_start is greater than _end", throw new jIO.util.jIOError("_start is greater than _end",
400); 400);
} }
start_index = Math.floor(start / UNITE); start_index = Math.floor(start / UNITE);
end_index = Math.floor(end / UNITE); end_index = Math.floor(end / UNITE) - 1;
if (end % UNITE === 0) { if (end % UNITE === 0) {
end_index -= 1; end_index -= 1;
} }
function resolver(result) {
for (i = start_index; i <= end_index; i += 1) { result_list.push(result);
promise_list.push( resolve(result_list);
handleGet(store.get(buildKeyPath([id, }
name, i]))) function getPart(i) {
return function (result) {
if (result) {
result_list.push(result);
}
i += 1;
handleGet(store,
buildKeyPath([id, name, i]),
(i <= end_index) ? getPart(i) : resolver,
reject
); );
};
} }
return RSVP.all(promise_list); getPart(start_index - 1)();
}
// XXX Should raise if key is not good
handleGet(transaction.objectStore("attachment"),
buildKeyPath([id, name]),
getBlob,
reject
);
});
}) })
.push(function (result_list) { .push(function (result_list) {
var array_buffer_list = [], var array_buffer_list = [],
...@@ -12668,19 +13134,24 @@ return new Parser; ...@@ -12668,19 +13134,24 @@ return new Parser;
}); });
}; };
function removeAttachment(transaction, id, name) { function removeAttachment(transaction, id, name, resolve, reject) {
return RSVP.all([
// XXX How to get the right attachment // XXX How to get the right attachment
handleRequest(transaction.objectStore("attachment")["delete"]( function deleteContent() {
handleCursor(
transaction.objectStore("blob").index("_id_attachment")
.openCursor(IDBKeyRange.only([id, name])),
deleteEntry,
resolve,
reject
);
}
handleRequest(
transaction.objectStore("attachment")["delete"](
buildKeyPath([id, name]) buildKeyPath([id, name])
)), ),
handleCursor(transaction.objectStore("blob").index("_id_attachment") deleteContent,
.openCursor(IDBKeyRange.only( reject
[id, name] );
)),
deleteEntry
)
]);
} }
IndexedDBStorage.prototype.putAttachment = function (id, name, blob) { IndexedDBStorage.prototype.putAttachment = function (id, name, blob) {
...@@ -12708,12 +13179,29 @@ return new Parser; ...@@ -12708,12 +13179,29 @@ return new Parser;
// Remove previous attachment // Remove previous attachment
transaction = openTransaction(db, ["attachment", "blob"], "readwrite"); transaction = openTransaction(db, ["attachment", "blob"], "readwrite");
return removeAttachment(transaction, id, name); return new RSVP.Promise(function (resolve, reject) {
}) function write() {
.push(function () { var len = blob_part.length - 1,
attachment_store = transaction.objectStore("attachment"),
var promise_list = [ blob_store = transaction.objectStore("blob");
handleRequest(transaction.objectStore("attachment").put({ function putBlobPart(i) {
return function () {
i += 1;
handleRequest(
blob_store.put({
"_key_path": buildKeyPath([id, name, i]),
"_id" : id,
"_attachment" : name,
"_part" : i,
"blob": blob_part[i]
}),
(i < len) ? putBlobPart(i) : resolve,
reject
);
};
}
handleRequest(
attachment_store.put({
"_key_path": buildKeyPath([id, name]), "_key_path": buildKeyPath([id, name]),
"_id": id, "_id": id,
"_attachment": name, "_attachment": name,
...@@ -12721,25 +13209,13 @@ return new Parser; ...@@ -12721,25 +13209,13 @@ return new Parser;
"content_type": blob.type, "content_type": blob.type,
"length": blob.size "length": blob.size
} }
})) }),
], putBlobPart(-1),
len = blob_part.length, reject
blob_store = transaction.objectStore("blob"),
i;
for (i = 0; i < len; i += 1) {
promise_list.push(
handleRequest(blob_store.put({
"_key_path": buildKeyPath([id, name,
i]),
"_id" : id,
"_attachment" : name,
"_part" : i,
"blob": blob_part[i]
}))
); );
} }
// Store all new data removeAttachment(transaction, id, name, write, reject);
return RSVP.all(promise_list); });
}); });
}; };
...@@ -12748,7 +13224,9 @@ return new Parser; ...@@ -12748,7 +13224,9 @@ return new Parser;
.push(function (db) { .push(function (db) {
var transaction = openTransaction(db, ["attachment", "blob"], var transaction = openTransaction(db, ["attachment", "blob"],
"readwrite"); "readwrite");
return removeAttachment(transaction, id, name); return new RSVP.Promise(function (resolve, reject) {
removeAttachment(transaction, id, name, resolve, reject);
});
}); });
}; };
...@@ -12766,24 +13244,29 @@ return new Parser; ...@@ -12766,24 +13244,29 @@ return new Parser;
(function (jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer) { (function (jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer) {
"use strict"; "use strict";
/*
The cryptography system used by this storage is AES-GCM.
Here is an example of how to generate a key to the json format:
// you the cryptography system used by this storage is AES-GCM. return new RSVP.Queue()
// here is an example of how to generate a key to the json format. .push(function () {
return crypto.subtle.generateKey({name: "AES-GCM", length: 256},
// var key, true, ["encrypt", "decrypt"]);
// jsonKey; })
// crypto.subtle.generateKey({name: "AES-GCM",length: 256}, .push(function (key) {
// (true), ["encrypt", "decrypt"]) return crypto.subtle.exportKey("jwk", key);
// .then(function(res){key = res;}); })
// .push(function (json_key) {
// window.crypto.subtle.exportKey("jwk", key) var jio = jIO.createJIO({
// .then(function(res){jsonKey = val}) type: "crypt",
// key: json_key,
//var storage = jIO.createJIO({type: "crypt", key: jsonKey, sub_storage: {storage_definition}
// sub_storage: {...}}); });
});
// find more informations about this cryptography system on Find more informations about this cryptography system on
// https://github.com/diafygi/webcrypto-examples#aes-gcm https://github.com/diafygi/webcrypto-examples#aes-gcm
*/
/** /**
* The JIO Cryptography Storage extension * The JIO Cryptography Storage extension
...@@ -12861,12 +13344,12 @@ return new Parser; ...@@ -12861,12 +13344,12 @@ return new Parser;
}) })
.push(function (dataURL) { .push(function (dataURL) {
//string->arraybuffer //string->arraybuffer
var strLen = dataURL.currentTarget.result.length, var strLen = dataURL.target.result.length,
buf = new ArrayBuffer(strLen), buf = new ArrayBuffer(strLen),
bufView = new Uint8Array(buf), bufView = new Uint8Array(buf),
i; i;
dataURL = dataURL.currentTarget.result; dataURL = dataURL.target.result;
for (i = 0; i < strLen; i += 1) { for (i = 0; i < strLen; i += 1) {
bufView[i] = dataURL.charCodeAt(i); bufView[i] = dataURL.charCodeAt(i);
} }
...@@ -12903,7 +13386,7 @@ return new Parser; ...@@ -12903,7 +13386,7 @@ return new Parser;
.push(function (coded) { .push(function (coded) {
var initializaton_vector; var initializaton_vector;
coded = coded.currentTarget.result; coded = coded.target.result;
initializaton_vector = new Uint8Array(coded.slice(0, 12)); initializaton_vector = new Uint8Array(coded.slice(0, 12));
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
...@@ -13122,7 +13605,7 @@ return new Parser; ...@@ -13122,7 +13605,7 @@ return new Parser;
return jIO.util.readBlobAsDataURL(blob); return jIO.util.readBlobAsDataURL(blob);
}) })
.push(function (strBlob) { .push(function (strBlob) {
argument_list[index + 2].push(strBlob.currentTarget.result); argument_list[index + 2].push(strBlob.target.result);
return; return;
}); });
} }
......
...@@ -749,6 +749,19 @@ if (typeof document.contains !== 'function') { ...@@ -749,6 +749,19 @@ if (typeof document.contains !== 'function') {
return new RSVP.Promise(itsANonResolvableTrap, canceller); return new RSVP.Promise(itsANonResolvableTrap, canceller);
} }
function promiseAnimationFrame() {
var request_id;
function canceller() {
window.cancelAnimationFrame(request_id);
}
function resolver(resolve) {
request_id = window.requestAnimationFrame(resolve);
}
return new RSVP.Promise(resolver, canceller);
}
function ajax(url) { function ajax(url) {
var xhr; var xhr;
function resolver(resolve, reject) { function resolver(resolve, reject) {
...@@ -793,13 +806,13 @@ if (typeof document.contains !== 'function') { ...@@ -793,13 +806,13 @@ if (typeof document.contains !== 'function') {
javascript_registration_dict = {}, javascript_registration_dict = {},
stylesheet_registration_dict = {}, stylesheet_registration_dict = {},
gadget_loading_klass_list = [], gadget_loading_klass_list = [],
loading_klass_promise,
renderJS, renderJS,
Monitor, Monitor,
scope_increment = 0, scope_increment = 0,
isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'), isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'),
is_page_unloaded = false, is_page_unloaded = false,
error_list = []; error_list = [],
all_dependency_loaded_deferred;
window.addEventListener('error', function (error) { window.addEventListener('error', function (error) {
error_list.push(error); error_list.push(error);
...@@ -1160,6 +1173,34 @@ if (typeof document.contains !== 'function') { ...@@ -1160,6 +1173,34 @@ if (typeof document.contains !== 'function') {
return this; return this;
}; };
RenderJSGadget.onLoop = function (callback, delay) {
if (delay === undefined) {
delay = 0;
}
this.__service_list.push(function () {
var queue_loop = new RSVP.Queue(),
wait = function () {
queue_loop
.push(function () {
return RSVP.delay(delay);
})
.push(function () {
// Only loop when the app has the focus
return promiseAnimationFrame();
})
.push(function () {
return callback.apply(this, []);
})
.push(function () {
wait();
});
};
wait();
return queue_loop;
});
return this;
};
function runJob(gadget, name, callback, argument_list) { function runJob(gadget, name, callback, argument_list) {
var job_promise = new RSVP.Queue() var job_promise = new RSVP.Queue()
.push(function () { .push(function () {
...@@ -1410,6 +1451,8 @@ if (typeof document.contains !== 'function') { ...@@ -1410,6 +1451,8 @@ if (typeof document.contains !== 'function') {
RenderJSGadget.declareService; RenderJSGadget.declareService;
RenderJSEmbeddedGadget.onEvent = RenderJSEmbeddedGadget.onEvent =
RenderJSGadget.onEvent; RenderJSGadget.onEvent;
RenderJSEmbeddedGadget.onLoop =
RenderJSGadget.onLoop;
RenderJSEmbeddedGadget.prototype = new RenderJSGadget(); RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget; RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;
...@@ -1473,6 +1516,8 @@ if (typeof document.contains !== 'function') { ...@@ -1473,6 +1516,8 @@ if (typeof document.contains !== 'function') {
RenderJSGadget.declareService; RenderJSGadget.declareService;
RenderJSIframeGadget.onEvent = RenderJSIframeGadget.onEvent =
RenderJSGadget.onEvent; RenderJSGadget.onEvent;
RenderJSIframeGadget.onLoop =
RenderJSGadget.onLoop;
RenderJSIframeGadget.prototype = new RenderJSGadget(); RenderJSIframeGadget.prototype = new RenderJSGadget();
RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget; RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;
...@@ -1521,7 +1566,8 @@ if (typeof document.contains !== 'function') { ...@@ -1521,7 +1566,8 @@ if (typeof document.contains !== 'function') {
// Create the communication channel with the iframe // Create the communication channel with the iframe
gadget_instance.__chan = Channel.build({ gadget_instance.__chan = Channel.build({
window: iframe.contentWindow, window: iframe.contentWindow,
origin: "*", // origin: (new URL(url, window.location)).origin,
origin: '*',
scope: "renderJS" scope: "renderJS"
}); });
...@@ -1536,12 +1582,8 @@ if (typeof document.contains !== 'function') { ...@@ -1536,12 +1582,8 @@ if (typeof document.contains !== 'function') {
params: [ params: [
method_name, method_name,
Array.prototype.slice.call(argument_list, 0)], Array.prototype.slice.call(argument_list, 0)],
success: function (s) { success: resolve,
resolve(s); error: reject
},
error: function (e) {
reject(e);
}
}); });
}); });
return new RSVP.Queue() return new RSVP.Queue()
...@@ -1842,6 +1884,8 @@ if (typeof document.contains !== 'function') { ...@@ -1842,6 +1884,8 @@ if (typeof document.contains !== 'function') {
RenderJSGadget.declareService; RenderJSGadget.declareService;
tmp_constructor.onEvent = tmp_constructor.onEvent =
RenderJSGadget.onEvent; RenderJSGadget.onEvent;
tmp_constructor.onLoop =
RenderJSGadget.onLoop;
tmp_constructor.prototype = new RenderJSGadget(); tmp_constructor.prototype = new RenderJSGadget();
tmp_constructor.prototype.constructor = tmp_constructor; tmp_constructor.prototype.constructor = tmp_constructor;
tmp_constructor.prototype.__path = url; tmp_constructor.prototype.__path = url;
...@@ -1874,9 +1918,6 @@ if (typeof document.contains !== 'function') { ...@@ -1874,9 +1918,6 @@ if (typeof document.contains !== 'function') {
gadget_model_defer_dict[url] = defer; gadget_model_defer_dict[url] = defer;
// Change the global variable to update the loading queue
loading_klass_promise = defer.promise;
// Fetch the HTML page and parse it // Fetch the HTML page and parse it
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
...@@ -1989,211 +2030,19 @@ if (typeof document.contains !== 'function') { ...@@ -1989,211 +2030,19 @@ if (typeof document.contains !== 'function') {
// Bootstrap process. Register the self gadget. // Bootstrap process. Register the self gadget.
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
function bootstrap() { // Detect when all JS dependencies have been loaded
var url = removeHash(window.location.href), all_dependency_loaded_deferred = new RSVP.defer();
TmpConstructor, // Manually initializes the self gadget if the DOMContentLoaded event
root_gadget, // is triggered before everything was ready.
loading_gadget_promise = new RSVP.Queue(), // (For instance, the HTML-tag for the self gadget gets inserted after
declare_method_count = 0, // page load)
embedded_channel, renderJS.manualBootstrap = function () {
notifyReady, all_dependency_loaded_deferred.resolve();
notifyDeclareMethod,
gadget_ready = false,
iframe_top_gadget,
last_acquisition_gadget,
declare_method_list_waiting = [],
gadget_failed = false,
gadget_error,
connection_ready = false;
// Create the gadget class for the current url
if (gadget_model_defer_dict.hasOwnProperty(url)) {
throw new Error("bootstrap should not be called twice");
}
loading_klass_promise = new RSVP.Promise(function (resolve, reject) {
last_acquisition_gadget = new RenderJSGadget();
last_acquisition_gadget.__acquired_method_dict = {
reportServiceError: function (param_list) {
letsCrash(param_list[0]);
}
};
// Stop acquisition on the last acquisition gadget
// Do not put this on the klass, as their could be multiple instances
last_acquisition_gadget.__aq_parent = function (method_name) {
throw new renderJS.AcquisitionError(
"No gadget provides " + method_name
);
};
//we need to determine tmp_constructor's value before exit bootstrap
//because of function : renderJS
//but since the channel checking is async,
//we can't use code structure like:
// if channel communication is ok
// tmp_constructor = RenderJSGadget
// else
// tmp_constructor = RenderJSEmbeddedGadget
if (window.self === window.top) {
// XXX Copy/Paste from declareGadgetKlass
TmpConstructor = function () {
RenderJSGadget.call(this);
};
TmpConstructor.declareMethod = RenderJSGadget.declareMethod;
TmpConstructor.declareJob = RenderJSGadget.declareJob;
TmpConstructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
RenderJSGadget.allowPublicAcquisition;
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice();
TmpConstructor.ready = RenderJSGadget.ready;
TmpConstructor.setState = RenderJSGadget.setState;
TmpConstructor.onStateChange = RenderJSGadget.onStateChange;
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice();
TmpConstructor.declareService =
RenderJSGadget.declareService;
TmpConstructor.onEvent =
RenderJSGadget.onEvent;
TmpConstructor.prototype = new RenderJSGadget();
TmpConstructor.prototype.constructor = TmpConstructor;
TmpConstructor.prototype.__path = url;
gadget_model_defer_dict[url] = {
promise: RSVP.resolve(TmpConstructor)
};
// Create the root gadget instance and put it in the loading stack
root_gadget = new TmpConstructor();
setAqParent(root_gadget, last_acquisition_gadget);
} else {
// Create the root gadget instance and put it in the loading stack
TmpConstructor = RenderJSEmbeddedGadget;
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice();
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice();
TmpConstructor.prototype.__path = url;
root_gadget = new RenderJSEmbeddedGadget();
setAqParent(root_gadget, last_acquisition_gadget);
// Create the communication channel
embedded_channel = Channel.build({
window: window.parent,
origin: "*",
scope: "renderJS",
onReady: function () {
var k;
iframe_top_gadget = false;
//Default: Define __aq_parent to inform parent window
root_gadget.__aq_parent =
TmpConstructor.prototype.__aq_parent = function (method_name,
argument_list, time_out) {
return new RSVP.Promise(function (resolve, reject) {
embedded_channel.call({
method: "acquire",
params: [
method_name,
argument_list
],
success: function (s) {
resolve(s);
},
error: function (e) {
reject(e);
},
timeout: time_out
});
});
};
// Channel is ready, so now declare Function
notifyDeclareMethod = function (name) {
declare_method_count += 1;
embedded_channel.call({
method: "declareMethod",
params: name,
success: function () {
declare_method_count -= 1;
notifyReady();
},
error: function () {
declare_method_count -= 1;
}
});
}; };
for (k = 0; k < declare_method_list_waiting.length; k += 1) { document.addEventListener('DOMContentLoaded',
notifyDeclareMethod(declare_method_list_waiting[k]); all_dependency_loaded_deferred.resolve, false);
}
declare_method_list_waiting = [];
// If Gadget Failed Notify Parent
if (gadget_failed) {
embedded_channel.notify({
method: "failed",
params: gadget_error
});
return;
}
connection_ready = true;
notifyReady();
//the channel is ok
//so bind calls to renderJS method on the instance
embedded_channel.bind("methodCall", function (trans, v) {
root_gadget[v[0]].apply(root_gadget, v[1])
.then(function (g) {
trans.complete(g);
}).fail(function (e) {
trans.error(e.toString());
});
trans.delayReturn(true);
});
}
});
// Notify parent about gadget instanciation
notifyReady = function () {
if ((declare_method_count === 0) && (gadget_ready === true)) {
embedded_channel.notify({method: "ready"});
}
};
// Inform parent gadget about declareMethod calls here.
notifyDeclareMethod = function (name) {
declare_method_list_waiting.push(name);
};
notifyDeclareMethod("getInterfaceList");
notifyDeclareMethod("getRequiredCSSList");
notifyDeclareMethod("getRequiredJSList");
notifyDeclareMethod("getPath");
notifyDeclareMethod("getTitle");
// Surcharge declareMethod to inform parent window
TmpConstructor.declareMethod = function (name, callback) {
var result = RenderJSGadget.declareMethod.apply(
this,
[name, callback]
);
notifyDeclareMethod(name);
return result;
};
TmpConstructor.declareService =
RenderJSGadget.declareService;
TmpConstructor.declareJob =
RenderJSGadget.declareJob;
TmpConstructor.onEvent =
RenderJSGadget.onEvent;
TmpConstructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
RenderJSGadget.allowPublicAcquisition;
iframe_top_gadget = true;
}
TmpConstructor.prototype.__acquired_method_dict = {}; function configureMutationObserver(TmpConstructor, url, root_gadget) {
gadget_loading_klass_list.push(TmpConstructor);
function init() {
// XXX HTML properties can only be set when the DOM is fully loaded // XXX HTML properties can only be set when the DOM is fully loaded
var settings = renderJS.parseGadgetHTMLDocument(document, url), var settings = renderJS.parseGadgetHTMLDocument(document, url),
j, j,
...@@ -2213,7 +2062,7 @@ if (typeof document.contains !== 'function') { ...@@ -2213,7 +2062,7 @@ if (typeof document.contains !== 'function') {
); );
} }
TmpConstructor.__template_element.appendChild(fragment); TmpConstructor.__template_element.appendChild(fragment);
RSVP.all([root_gadget.getRequiredJSList(), return RSVP.all([root_gadget.getRequiredJSList(),
root_gadget.getRequiredCSSList()]) root_gadget.getRequiredCSSList()])
.then(function (all_list) { .then(function (all_list) {
var i, var i,
...@@ -2295,21 +2144,156 @@ if (typeof document.contains !== 'function') { ...@@ -2295,21 +2144,156 @@ if (typeof document.contains !== 'function') {
observer.observe(target, config); observer.observe(target, config);
return root_gadget; return root_gadget;
}).then(resolve, function (e) {
reject(e);
console.error(e);
throw e;
}); });
} }
document.addEventListener('DOMContentLoaded', init, false);
});
loading_gadget_promise function createLastAcquisitionGadget() {
var last_acquisition_gadget = new RenderJSGadget();
last_acquisition_gadget.__acquired_method_dict = {
reportServiceError: function (param_list) {
letsCrash(param_list[0]);
}
};
// Stop acquisition on the last acquisition gadget
// Do not put this on the klass, as their could be multiple instances
last_acquisition_gadget.__aq_parent = function (method_name) {
throw new renderJS.AcquisitionError(
"No gadget provides " + method_name
);
};
return last_acquisition_gadget;
}
/*
function notifyAllMethodToParent() {
;
}
*/
function createLoadingGadget(url) {
var TmpConstructor,
root_gadget,
embedded_channel,
notifyDeclareMethod,
declare_method_list_waiting,
loading_result,
channel_defer,
real_result_list;
// gadget_failed = false,
// connection_ready = false;
// Create the gadget class for the current url
if (gadget_model_defer_dict.hasOwnProperty(url)) {
throw new Error("bootstrap should not be called twice");
}
// Create the root gadget instance and put it in the loading stack
TmpConstructor = RenderJSEmbeddedGadget;
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice();
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice();
TmpConstructor.prototype.__path = url;
root_gadget = new RenderJSEmbeddedGadget();
setAqParent(root_gadget, createLastAcquisitionGadget());
declare_method_list_waiting = [
"getInterfaceList",
"getRequiredCSSList",
"getRequiredJSList",
"getPath",
"getTitle"
];
// Inform parent gadget about declareMethod calls here.
notifyDeclareMethod = function (name) {
declare_method_list_waiting.push(name);
};
real_result_list = [TmpConstructor, root_gadget, embedded_channel,
declare_method_list_waiting];
if (window.self === window.top) {
loading_result = real_result_list;
} else {
channel_defer = RSVP.defer();
loading_result = RSVP.any([
channel_defer.promise,
new RSVP.Queue()
.push(function () {
// Expect the channel to parent to be usable after 1 second
// If not, consider the gadget as the root
// Drop all iframe channel communication
return RSVP.delay(1000);
})
.push(function () { .push(function () {
return loading_klass_promise; real_result_list[2] = undefined;
return real_result_list;
}) })
.push(function (root_gadget) { ]);
var i; // Create the communication channel
embedded_channel = Channel.build({
window: window.parent,
origin: "*",
scope: "renderJS",
onReady: function () {
var k,
len;
// Channel is ready, so now declare all methods
notifyDeclareMethod = function (name) {
declare_method_list_waiting.push(
new RSVP.Promise(function (resolve, reject) {
embedded_channel.call({
method: "declareMethod",
params: name,
success: resolve,
error: reject
});
})
);
};
len = declare_method_list_waiting.length;
for (k = 0; k < len; k += 1) {
notifyDeclareMethod(declare_method_list_waiting[k]);
}
channel_defer.resolve(real_result_list);
}
});
real_result_list[2] = embedded_channel;
}
// Surcharge declareMethod to inform parent window
TmpConstructor.declareMethod = function (name, callback) {
var result = RenderJSGadget.declareMethod.apply(
this,
[name, callback]
);
notifyDeclareMethod(name);
return result;
};
TmpConstructor.declareService =
RenderJSGadget.declareService;
TmpConstructor.declareJob =
RenderJSGadget.declareJob;
TmpConstructor.onEvent =
RenderJSGadget.onEvent;
TmpConstructor.onLoop =
RenderJSGadget.onLoop;
TmpConstructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
RenderJSGadget.allowPublicAcquisition;
TmpConstructor.prototype.__acquired_method_dict = {};
gadget_loading_klass_list.push(TmpConstructor);
return loading_result;
}
function triggerReadyList(TmpConstructor, root_gadget) {
// XXX Probably duplicated
var i,
ready_queue = new RSVP.Queue();
function ready_wrapper() { function ready_wrapper() {
return root_gadget; return root_gadget;
...@@ -2323,45 +2307,108 @@ if (typeof document.contains !== 'function') { ...@@ -2323,45 +2307,108 @@ if (typeof document.contains !== 'function') {
return startService(this); return startService(this);
}); });
loading_gadget_promise.push(ready_wrapper); ready_queue.push(ready_wrapper);
for (i = 0; i < TmpConstructor.__ready_list.length; i += 1) { for (i = 0; i < TmpConstructor.__ready_list.length; i += 1) {
// Put a timeout? // Put a timeout?
loading_gadget_promise ready_queue
.push(ready_executable_wrapper(TmpConstructor.__ready_list[i])) .push(ready_executable_wrapper(TmpConstructor.__ready_list[i]))
// Always return the gadget instance after ready function // Always return the gadget instance after ready function
.push(ready_wrapper); .push(ready_wrapper);
} }
return ready_queue;
}
function finishAqParentConfiguration(TmpConstructor, root_gadget,
embedded_channel) {
// Define __aq_parent to inform parent window
root_gadget.__aq_parent =
TmpConstructor.prototype.__aq_parent = function (method_name,
argument_list,
time_out) {
return new RSVP.Promise(function (resolve, reject) {
embedded_channel.call({
method: "acquire",
params: [
method_name,
argument_list
],
success: function (s) {
resolve(s);
},
error: function (e) {
reject(e);
},
timeout: time_out
}); });
if (window.self === window.top) {
loading_gadget_promise
.fail(function (e) {
letsCrash(e);
throw e;
}); });
} else { };
// Inform parent window that gadget is correctly loaded
loading_gadget_promise // bind calls to renderJS method on the instance
.then(function () { embedded_channel.bind("methodCall", function (trans, v) {
gadget_ready = true; root_gadget[v[0]].apply(root_gadget, v[1])
if (connection_ready) { .push(function (g) {
notifyReady(); trans.complete(g);
}, function (e) {
trans.error(e.toString());
});
trans.delayReturn(true);
});
} }
function bootstrap(url) {
// Create the loading gadget
var wait_for_gadget_loaded = createLoadingGadget(url),
TmpConstructor,
root_gadget,
embedded_channel,
declare_method_list_waiting;
return new RSVP.Queue()
.push(function () {
// Wait for the loading gadget to be created
return wait_for_gadget_loaded;
})
.push(function (result_list) {
TmpConstructor = result_list[0];
root_gadget = result_list[1];
embedded_channel = result_list[2];
declare_method_list_waiting = result_list[3];
// Wait for all the gadget dependencies to be loaded
return all_dependency_loaded_deferred.promise;
}) })
.fail(function (e) { .push(function () {
//top gadget in iframe // Wait for all methods to be correctly declared
if (iframe_top_gadget) { return RSVP.all(declare_method_list_waiting);
gadget_failed = true; })
gadget_error = e.toString(); .push(function (result_list) {
if (embedded_channel !== undefined) {
finishAqParentConfiguration(TmpConstructor, root_gadget,
embedded_channel);
}
// Check all DOM modifications to correctly start/stop services
return configureMutationObserver(TmpConstructor, url, root_gadget);
})
.push(function () {
// Trigger all ready functions
return triggerReadyList(TmpConstructor, root_gadget);
})
.push(function () {
if (embedded_channel !== undefined) {
embedded_channel.notify({method: "ready"});
}
})
.push(undefined, function (e) {
letsCrash(e); letsCrash(e);
} else { if (embedded_channel !== undefined) {
embedded_channel.notify({method: "failed", params: e.toString()}); embedded_channel.notify({method: "failed", params: e.toString()});
} }
throw e; throw e;
}); });
} }
} bootstrap(
bootstrap(); removeHash(window.location.href)
);
}(document, window, RSVP, DOMParser, Channel, MutationObserver, Node, }(document, window, RSVP, DOMParser, Channel, MutationObserver, Node,
FileReader, Blob, navigator, Event, URL)); FileReader, Blob, navigator, Event, URL));
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment