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,61 +3,65 @@ ...@@ -3,61 +3,65 @@
<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}}">
<label for="toggle-all" class="toggle-label">Mark all as complete</label>
<input id="toggle-all" class="toggle-all" type="checkbox" {{#if all_checked}}checked="true"{{/if}}>
<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}}"
data-jio-id="{{this.id}}">
<div class="view {{#if this.edit}}hidden{{/if}}">
<input class="toggle" type="checkbox"{{#if this.completed}} checked="true"{{/if}}>
<label class="todo-label">{{this.title}}</label>
<button class="destroy"></button>
</div>
<input class="edit{{#unless this.editing}} hidden{{/unless}}">
</li>
</script>
</head> </head>
<body> <body>
<section id="todoapp" class="todoapp">
<header id="header" class="header">
<h1>todos</h1>
<form id="todo-form">
<input id="new-todo" class="new-todo" placeholder="What needs to be done?" autofocus>
</form>
</header>
<main class="handlebars">
</main>
</section>
<footer id="info" class="info">
<p>Double-click to edit a todo</p>
</footer>
<div data-gadget-url="gadget_model.html" <div data-gadget-url="gadget_model.html"
data-gadget-scope="model" data-gadget-scope="model"
data-gadget-sandbox="public"> data-gadget-sandbox="public">
</div> </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>
<ul class="todo-list">
{{#each todo_list}}
<li class="todo-item {{#if this.completed}}completed{{/if}} {{#if this.editing}}editing{{/if}}"
data-jio-id="{{this.id}}">
<div class="view {{#if this.edit}}hidden{{/if}}">
<input class="toggle" type="checkbox"{{#if this.completed}} checked="true"{{/if}}>
<label class="todo-label">{{this.title}}</label>
<button class="destroy"></button>
</div>
<input class="edit{{#unless this.editing}} hidden{{/unless}}">
</li>
{{/each}}
</ul>
</section>
<footer class="footer {{#unless todo_exists}}hidden{{/unless}}">
<span class="todo-count">{{todo_count}}</span>
<div class="filters">
<a href="#/" class="selected">All</a>
<a href="#/active">Active</a>
<a href="#/completed">Completed</a>
</div>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
</script>
</body> </body>
</html> </html>
\ No newline at end of file
/*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');
// initialize the router and set the model on the main gadget for
// Create a new empty element for the router gadget. // easier reference. Set templates, then render first DOM.
var div = document.createElement('div'); return new RSVP.Queue()
gadget.element.appendChild(div); .push(function() {
return gadget.getDeclaredGadget('model');
// Compile the Handlebars template only once, on page load. })
handlebars_template = Handlebars.compile(temp.innerHTML); .push(function(response) {
var div = document.createElement(DIV);
// Declare the router gadget in JavaScript instead of HTML. gadget.element.appendChild(div);
return gadget.declareGadget('gadget_router.html', { gadget.property_dict = {
scope: 'router', 'model': response
sandbox: 'public', };
element: div return gadget.declareGadget('gadget_router.html', {
}) scope: 'router',
// Render the state for the first time. sandbox: 'public',
.push(function () { element: div
return gadget.changeState({update: true}); });
})
.push(function() {
gadget.template_dict = {
'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
});
});
})
/////////////////////////////
// onStateChange
/////////////////////////////
.onStateChange(function(modification_dict) {
var gadget = this;
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) {
// Get the list of todos from storage. var count_dict = response_list[0];
.push(function (todo_list) { var todo_list = response_list[1];
var plural = todo_list.length === 1 ? ' item' : ' items'; var plural = todo_list.length === 1 ? ITEM : ITEMS;
var focus_query = '.new-todo'; var focus_selector = INPUT_SELECTOR;
var edit_value = ''; var edit_value = STR;
var post_value = ''; var input_value = STR;
var toggle_all = getElem(element, ALL);
// If a todo is currently being edited, var all_checked = false;
// set the focus to its edit input. var count_content = count_dict.active.toString() + plural;
// Otherwise, the focus remains on the new todo input. var all_completed = count_dict.active === count_dict.total;
if (gadget.state.editing_jio_id) {
focus_query = 'li[data-jio-id="' + // tick all
gadget.state.editing_jio_id + '"] .edit'; if (toggle_all) {
toggle_all.checked =
all_checked =
getAllChecked(todo_list);
} }
// If the new todo input has not yet been submitted and // keep focus on todo being edited
// it exists, then keep its current value in post_value. if (state.editing_jio_id) {
if (!modification_dict.hasOwnProperty('clear_input') && focus_selector = setSelector(state.editing_jio_id);
gadget.element.querySelector('.new-todo')) {
post_value =
gadget.element.querySelector('.new-todo').value;
} }
// Set at most one todo as currently being edited. // set todo being edited
for (var i = 0; i < todo_list.length; i += 1) { todo_list.forEach(function(todo) {
if (todo_list[i].id === gadget.state.editing_jio_id) { if (todo.id === state.editing_jio_id) {
todo_list[i].editing = true; todo.editing = true;
edit_value = todo_list[i].title; edit_value = todo.title;
} else { } else {
todo_list[i].editing = false; todo.editing = false;
} }
});
// clear input
if (!modification_dict.hasOwnProperty('clear_input') &&
getElem(element, INPUT_SELECTOR)) {
input_value = getElem(element, INPUT_SELECTOR).value;
} }
// Apply the Handlebars template on the todo list. // set initial DOM
gadget.element.querySelector('.handlebars').innerHTML = if (modification_dict.hasOwnProperty('create')) {
handlebars_template({ getElem(element, '.handlebars').innerHTML =
todo_list: todo_list, temp.list_template({
todo_exists: todo_count_dict.total >= 1, 'todo_exists': count_dict.total >= 1,
todo_count: 'todo_count': count_content,
todo_count_dict.active.toString() + plural, 'all_completed': all_completed,
all_completed: todo_count_dict.active === 0 'all_checked': getAllChecked(todo_list)
});
dict.list = getElem(element, LIST);
todo_list.forEach(function(item) {
setTodo(dict.list, item, temp.item_template);
}); });
}
// Focus the proper element and copy the previous values // Update DOM
// of the currently editing input and the new todo input if (modification_dict.hasOwnProperty('update') &&
// back into them, since Handlebars reset all input values dict.list)
gadget.element.querySelector(focus_query).focus(); {
if (edit_value) { makeList(getElems(dict.list, 0, LI))
gadget.element.querySelector(focus_query).value = .forEach(function(li) {
edit_value; 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 {
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;
});
// new items
todo_list.forEach(function(item) {
setTodo(dict.list, item, temp.item_template);
});
// clear completed
if (all_completed) {
getElem(element, CLEAR).classList.add(HIDDEN);
} 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 (post_value) {
gadget.element.querySelector('.new-todo').value = // if editing, set focus and input value
post_value; if (edit_value) {
getElem(element, focus_selector).focus();
getElem(element, focus_selector).value = edit_value;
} }
// set filter
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
/////////////////////////////
// Post a new todo when the new todo input is submitted. // new todo
.onEvent('submit', function (event) { .onEvent('submit', function(event) {
var gadget = this; return this.storeItem(event.target.elements[0].value.trim());
var item = event.target.elements[0].value.trim();
// Trim the input and reject blank values.
if (!item) {
return;
}
// Change clear_input in state to clear the new todo input.
return gadget.getDeclaredGadget('model')
.push(function (model_gadget) {
return model_gadget.postTodo(item);
})
.push(function () {
return gadget.changeState({clear_input: true});
});
}, false, true) }, false, true)
// Do the correct action when anything is clicked once. // edit todo
.onEvent('click', function (event) { .onEvent('dblclick', function(event) {
var gadget = this; var gadget = this;
var todo_item = event.target.parentElement.parentElement; var dict = gadget.property_dict;
var jio_id = todo_item.getAttribute('data-jio-id'); var target = event.target;
var jio_id = getId(event.target, 2);
// Delegate all responsibility to the model gadget. var input = target.parentElement.nextElementSibling;
return gadget.getDeclaredGadget('model')
.push(function (model_gadget) { if (target.className !== 'todo-label') {
switch (event.target.className) { return;
}
// Set completed to the opposite of its current status.
case 'toggle': return new RSVP.Queue()
return model_gadget.toggleOne( .push(function() {
jio_id, return gadget.changeState({
!todo_item.classList.contains('completed') 'update': true,
); 'editing_jio_id': jio_id
});
// Set completed to the state of the toggle all checkbox. })
case 'toggle-all': .push(function() {
return model_gadget.toggleAll(event.target.checked); dict.defer = new RSVP.defer();
// Set completed to the opposite of the current state // ESC resolves the defer and prevents storing
// of the checkbox when the label is clicked, because return RSVP.any([
// the event is sent before the checkbox changes state dict.defer.promise,
case 'toggle-label': promiseEventListener(input, 'blur', true)
return model_gadget.toggleAll( ]);
!gadget.element })
.querySelector('.toggle-all').checked .push(function(event) {
); var target;
// Remove the todo with the given ID. // defer was here
case 'destroy': if (!event) {
return model_gadget.removeOne(jio_id); input.blur();
return;
// Remove all completed todos. }
case 'clear-completed':
return model_gadget.removeCompleted(); target = event.target;
if (target.value === '') {
// If the user clicked anywhere else, don't do anything, return dict.model.removeOne(getId(target, 1));
// unless it was outside the currently editing input. }
default: return gadget.storeItem({
if (gadget.state.editing_jio_id && 'title': target.value.trim()
event.target.className !== 'edit') { }, jio_id);
return 'cancel editing'; })
} .push(function() {
return 'default'; return gadget.changeState({
} 'update': true,
}) 'editing_jio_id': ''
});
// Only update the user if the user clicked outside the default. });
.push(function (path) {
if (path !== 'default') {
return gadget.changeState({
update: true,
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. if (event.keyCode === ESCAPE_KEY) {
.onEvent('keydown', function (event) { return new RSVP.Queue()
var gadget = this; .push(function() {
return gadget.changeState({
// Reset everything if the escape key is pressed. 'update': true,
if (event.target.className === 'edit') { 'editing_jio_id': STR
if (event.keyCode === ESCAPE_KEY) { });
return gadget.changeState({ })
update: true, .push(function() {
editing_jio_id: '' 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);
this._signature_hash = "_replicate_" + generateHash( if (spec.hasOwnProperty('signature_sub_storage')) {
stringify(spec.local_sub_storage) + this._signature_sub_storage = jIO.createJIO(spec.signature_sub_storage);
stringify(spec.remote_sub_storage) + this._custom_signature_sub_storage = true;
stringify(this._query_options) } else {
); this._signature_hash = "_replicate_" + generateHash(
this._signature_sub_storage = jIO.createJIO({ stringify(spec.local_sub_storage) +
type: "document", stringify(spec.remote_sub_storage) +
document_id: this._signature_hash, stringify(this._query_options)
sub_storage: spec.signature_storage || spec.local_sub_storage );
}); this._signature_sub_storage = jIO.createJIO({
type: "query",
sub_storage: {
type: "document",
document_id: this._signature_hash,
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,420 +8641,726 @@ return new Parser; ...@@ -8596,420 +8641,726 @@ 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
skip_document_dict[context._signature_hash] = null;
function propagateAttachmentDeletion(skip_attachment_dict, function pushAndExecute(queue) {
destination, queue
id, name) {
return destination.removeAttachment(id, name)
.push(function () {
return context._signature_sub_storage.removeAttachment(id, name);
})
.push(function () { .push(function () {
skip_attachment_dict[name] = null; 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) {
function propagateAttachmentModification(skip_attachment_dict, result_promise_list.push(new RSVP.Queue());
destination, pushAndExecute(result_promise_list[i]);
blob, hash, id, name) { }
return destination.putAttachment(id, name, blob) if (number_queue > 1) {
.push(function () { return RSVP.all(result_promise_list);
return context._signature_sub_storage.putAttachment(id, name,
JSON.stringify({
hash: hash
}));
})
.push(function () {
skip_attachment_dict[name] = null;
});
} }
return result_promise_list[0];
}
function checkAndPropagateAttachment(skip_attachment_dict, function callAllDocsOnStorage(context, storage, cache, cache_key) {
status_hash, local_hash, blob, return new RSVP.Queue()
source, destination, id, name, .push(function () {
conflict_force, conflict_revert, if (!cache.hasOwnProperty(cache_key)) {
conflict_ignore) { return storage.allDocs(context._query_options)
var remote_blob; .push(function (result) {
return destination.getAttachment(id, name) var i,
.push(function (result) { cache_entry = {};
remote_blob = result; for (i = 0; i < result.data.total_rows; i += 1) {
return jIO.util.readBlobAsArrayBuffer(remote_blob); cache_entry[result.data.rows[i].id] = result.data.rows[i].value;
}) }
.push(function (evt) { cache[cache_key] = cache_entry;
return generateHashFromArrayBuffer( });
evt.target.result }
); })
}, function (error) { .push(function () {
if ((error instanceof jIO.util.jIOError) && return cache[cache_key];
(error.status_code === 404)) { });
remote_blob = null; }
return null;
} function propagateAttachmentDeletion(context, skip_attachment_dict,
throw error; destination,
}) id, name) {
.push(function (remote_hash) { return destination.removeAttachment(id, name)
if (local_hash === remote_hash) { .push(function () {
// Same modifications on both side return context._signature_sub_storage.removeAttachment(id, name);
if (local_hash === null) { })
// Deleted on both side, drop signature .push(function () {
return context._signature_sub_storage.removeAttachment(id, name) skip_attachment_dict[name] = null;
.push(function () { });
skip_attachment_dict[id] = null; }
});
} function propagateAttachmentModification(context, skip_attachment_dict,
destination,
blob, hash, id, name) {
return destination.putAttachment(id, name, blob)
.push(function () {
return context._signature_sub_storage.putAttachment(id, name,
JSON.stringify({
hash: hash
}));
})
.push(function () {
skip_attachment_dict[name] = null;
});
}
return context._signature_sub_storage.putAttachment(id, name, function checkAndPropagateAttachment(context,
JSON.stringify({ skip_attachment_dict,
hash: local_hash status_hash, local_hash, blob,
})) source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore) {
var remote_blob;
return destination.getAttachment(id, name)
.push(function (result) {
remote_blob = result;
return jIO.util.readBlobAsArrayBuffer(remote_blob);
})
.push(function (evt) {
return generateHashFromArrayBuffer(
evt.target.result
);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
remote_blob = null;
return null;
}
throw error;
})
.push(function (remote_hash) {
if (local_hash === remote_hash) {
// Same modifications on both side
if (local_hash === null) {
// Deleted on both side, drop signature
return context._signature_sub_storage.removeAttachment(id, name)
.push(function () { .push(function () {
skip_document_dict[id] = null; skip_attachment_dict[name] = null;
}); });
} }
if ((remote_hash === status_hash) || (conflict_force === true)) { return context._signature_sub_storage.putAttachment(id, name,
// Modified only locally. No conflict or force JSON.stringify({
if (local_hash === null) { hash: local_hash
// Deleted locally }))
return propagateAttachmentDeletion(skip_attachment_dict, .push(function () {
destination, skip_attachment_dict[name] = null;
id, name); });
} }
return propagateAttachmentModification(skip_attachment_dict,
destination, blob,
local_hash, id, name);
}
// Conflict cases if ((remote_hash === status_hash) || (conflict_force === true)) {
if (conflict_ignore === true) { // Modified only locally. No conflict or force
return; if (local_hash === null) {
// Deleted locally
return propagateAttachmentDeletion(context, skip_attachment_dict,
destination,
id, name);
} }
return propagateAttachmentModification(context,
skip_attachment_dict,
destination, blob,
local_hash, id, name);
}
if ((conflict_revert === true) || (local_hash === null)) { // Conflict cases
// Automatically resolve conflict or force revert if (conflict_ignore === true) {
if (remote_hash === null) { return;
// Deleted remotely }
return propagateAttachmentDeletion(skip_attachment_dict,
source, id, name);
}
return propagateAttachmentModification(
skip_attachment_dict,
source,
remote_blob,
remote_hash,
id,
name
);
}
// Minimize conflict if it can be resolved if ((conflict_revert === true) || (local_hash === null)) {
// Automatically resolve conflict or force revert
if (remote_hash === null) { if (remote_hash === null) {
// Copy remote modification remotely // Deleted remotely
return propagateAttachmentModification(skip_attachment_dict, return propagateAttachmentDeletion(context, skip_attachment_dict,
destination, blob, source, id, name);
local_hash, id, name);
}
throw new jIO.util.jIOError("Conflict on '" + id +
"' with attachment '" +
name + "'",
409);
});
}
function checkAttachmentSignatureDifference(skip_attachment_dict,
queue, source,
destination, id, name,
conflict_force,
conflict_revert,
conflict_ignore,
is_creation, is_modification) {
var blob,
status_hash;
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([
source.getAttachment(id, name),
{hash: null}
]);
}
if (is_modification === true) {
return RSVP.all([
source.getAttachment(id, name),
context._signature_sub_storage.getAttachment(
id,
name,
{format: 'json'}
)
]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkAttachmentSignatureDifference",
409);
})
.push(function (result_list) {
blob = result_list[0];
status_hash = result_list[1].hash;
return jIO.util.readBlobAsArrayBuffer(blob);
})
.push(function (evt) {
var array_buffer = evt.target.result,
local_hash = generateHashFromArrayBuffer(array_buffer);
if (local_hash !== status_hash) {
return checkAndPropagateAttachment(skip_attachment_dict,
status_hash, local_hash, blob,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore);
} }
}); return propagateAttachmentModification(
} context,
skip_attachment_dict,
function checkAttachmentLocalDeletion(skip_attachment_dict, source,
queue, destination, id, name, source, remote_blob,
conflict_force, conflict_revert, remote_hash,
conflict_ignore) { id,
var status_hash; name
queue );
.push(function () { }
return context._signature_sub_storage.getAttachment(id, name,
{format: 'json'});
})
.push(function (result) {
status_hash = result.hash;
return checkAndPropagateAttachment(skip_attachment_dict,
status_hash, null, null,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore);
});
}
function pushDocumentAttachment(skip_attachment_dict, id, source, // Minimize conflict if it can be resolved
destination, options) { if (remote_hash === null) {
var queue = new RSVP.Queue(); // Copy remote modification remotely
return propagateAttachmentModification(context,
skip_attachment_dict,
destination, blob,
local_hash, id, name);
}
throw new jIO.util.jIOError("Conflict on '" + id +
"' with attachment '" +
name + "'",
409);
});
}
return queue function checkAttachmentSignatureDifference(queue, context,
.push(function () { skip_attachment_dict,
source,
destination, id, name,
conflict_force,
conflict_revert,
conflict_ignore,
is_creation, is_modification) {
var blob,
status_hash;
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([ return RSVP.all([
source.allAttachments(id) source.getAttachment(id, name),
.push(undefined, function (error) { {hash: null}
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
}),
context._signature_sub_storage.allAttachments(id)
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
})
]); ]);
}) }
.push(function (result_list) { if (is_modification === true) {
var local_dict = {}, return RSVP.all([
signature_dict = {}, source.getAttachment(id, name),
is_modification, context._signature_sub_storage.getAttachment(
is_creation, id,
key; name,
for (key in result_list[0]) { {format: 'json'}
if (result_list[0].hasOwnProperty(key)) { )
if (!skip_attachment_dict.hasOwnProperty(key)) { ]);
local_dict[key] = null; }
} throw new jIO.util.jIOError("Unexpected call of"
+ " checkAttachmentSignatureDifference",
409);
})
.push(function (result_list) {
blob = result_list[0];
status_hash = result_list[1].hash;
return jIO.util.readBlobAsArrayBuffer(blob);
})
.push(function (evt) {
var array_buffer = evt.target.result,
local_hash = generateHashFromArrayBuffer(array_buffer);
if (local_hash !== status_hash) {
return checkAndPropagateAttachment(context,
skip_attachment_dict,
status_hash, local_hash, blob,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore);
}
});
}
function checkAttachmentLocalDeletion(queue, context,
skip_attachment_dict,
destination, id, name, source,
conflict_force, conflict_revert,
conflict_ignore) {
var status_hash;
queue
.push(function () {
return context._signature_sub_storage.getAttachment(id, name,
{format: 'json'});
})
.push(function (result) {
status_hash = result.hash;
return checkAndPropagateAttachment(context,
skip_attachment_dict,
status_hash, null, null,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore);
});
}
function pushDocumentAttachment(context,
skip_attachment_dict, id, source,
destination, signature_allAttachments,
options) {
var local_dict = {},
signature_dict = {};
return source.allAttachments(id)
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
})
.push(function (source_allAttachments) {
var is_modification,
is_creation,
key,
argument_list = [];
for (key in source_allAttachments) {
if (source_allAttachments.hasOwnProperty(key)) {
if (!skip_attachment_dict.hasOwnProperty(key)) {
local_dict[key] = null;
} }
} }
for (key in result_list[1]) { }
if (result_list[1].hasOwnProperty(key)) { for (key in signature_allAttachments) {
if (!skip_attachment_dict.hasOwnProperty(key)) { if (signature_allAttachments.hasOwnProperty(key)) {
signature_dict[key] = null; if (!skip_attachment_dict.hasOwnProperty(key)) {
} signature_dict[key] = null;
} }
} }
}
for (key in local_dict) { for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) { if (local_dict.hasOwnProperty(key)) {
is_modification = signature_dict.hasOwnProperty(key) is_modification = signature_dict.hasOwnProperty(key)
&& 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_modification === true || is_creation === true) { if (is_modification === true || is_creation === true) {
checkAttachmentSignatureDifference(skip_attachment_dict, argument_list.push([undefined,
queue, source, context,
destination, id, key, skip_attachment_dict,
options.conflict_force, source,
options.conflict_revert, destination, id, key,
options.conflict_ignore, options.conflict_force,
is_creation, options.conflict_revert,
is_modification); options.conflict_ignore,
is_creation,
is_modification]);
}
}
}
return dispatchQueue(
context,
checkAttachmentSignatureDifference,
argument_list,
context._parallel_operation_attachment_amount
);
})
.push(function () {
var key, argument_list = [];
if (options.check_deletion === true) {
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (!local_dict.hasOwnProperty(key)) {
argument_list.push([undefined,
context,
skip_attachment_dict,
destination, id, key,
source,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore]);
} }
} }
} }
if (options.check_deletion === true) { return dispatchQueue(
for (key in signature_dict) { context,
if (signature_dict.hasOwnProperty(key)) { checkAttachmentLocalDeletion,
if (!local_dict.hasOwnProperty(key)) { argument_list,
checkAttachmentLocalDeletion(skip_attachment_dict, context._parallel_operation_attachment_amount
queue, destination, id, key, );
source, }
options.conflict_force, });
options.conflict_revert, }
options.conflict_ignore);
} 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);
} }
var skip_attachment_dict = {};
return new RSVP.Queue()
.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 ||
context._check_local_attachment_creation ||
context._check_local_attachment_deletion) {
return pushDocumentAttachment(
context,
skip_attachment_dict,
id,
context._local_sub_storage,
context._remote_sub_storage,
signature_allAttachments,
{
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification:
context._check_local_attachment_modification,
check_creation: context._check_local_attachment_creation,
check_deletion: context._check_local_attachment_deletion
}
)
.push(function () {
return signature_allAttachments;
});
}
return signature_allAttachments;
})
.push(function (signature_allAttachments) {
if (context._check_remote_attachment_modification ||
context._check_remote_attachment_creation ||
context._check_remote_attachment_deletion) {
return pushDocumentAttachment(
context,
skip_attachment_dict,
id,
context._remote_sub_storage,
context._local_sub_storage,
signature_allAttachments,
{
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification:
context._check_remote_attachment_modification,
check_creation: context._check_remote_attachment_creation,
check_deletion: context._check_remote_attachment_deletion
}
);
}
});
}
function propagateModification(context, source, destination, doc, hash, id,
skip_document_dict,
skip_deleted_document_dict,
options) {
var result = new RSVP.Queue(),
post_id,
to_skip = true,
from_local;
if (options === undefined) {
options = {};
}
from_local = options.from_local;
function repairDocumentAttachment(id) { if (doc === null) {
var skip_attachment_dict = {}; result
return new RSVP.Queue()
.push(function () { .push(function () {
if (context._check_local_attachment_modification || return source.get(id);
context._check_local_attachment_creation ||
context._check_local_attachment_deletion) {
return pushDocumentAttachment(
skip_attachment_dict,
id,
context._local_sub_storage,
context._remote_sub_storage,
{
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification:
context._check_local_attachment_modification,
check_creation: context._check_local_attachment_creation,
check_deletion: context._check_local_attachment_deletion
}
);
}
}) })
.push(function () { .push(function (source_doc) {
if (context._check_remote_attachment_modification || doc = source_doc;
context._check_remote_attachment_creation || }, function (error) {
context._check_remote_attachment_deletion) { if ((error instanceof jIO.util.jIOError) &&
return pushDocumentAttachment( (error.status_code === 404)) {
skip_attachment_dict, throw new SkipError(id);
id,
context._remote_sub_storage,
context._local_sub_storage,
{
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification:
context._check_remote_attachment_modification,
check_creation: context._check_remote_attachment_creation,
check_deletion: context._check_remote_attachment_deletion
}
);
} }
throw error;
}); });
} }
if (options.use_post) {
result
.push(function () {
return destination.post(doc);
})
.push(function (new_id) {
to_skip = false;
post_id = new_id;
return source.put(post_id, doc);
})
.push(function () {
// Copy all attachments
// This is not related to attachment replication
// It's just about not losing user data
return source.allAttachments(id);
})
.push(function (attachment_dict) {
var key,
copy_queue = new RSVP.Queue();
function propagateModification(source, destination, doc, hash, id, function copyAttachment(name) {
options) { copy_queue
var result, .push(function () {
post_id, return source.getAttachment(id, name);
to_skip = true; })
if (options === undefined) { .push(function (blob) {
options = {}; return source.putAttachment(post_id, name, blob);
} });
if (options.use_post) { }
result = destination.post(doc)
.push(function (new_id) {
to_skip = false;
post_id = new_id;
return source.put(post_id, doc);
})
.push(function () {
// Copy all attachments
// This is not related to attachment replication
// It's just about not losing user data
return source.allAttachments(id);
})
.push(function (attachment_dict) {
var key,
copy_queue = new RSVP.Queue();
function copyAttachment(name) {
copy_queue
.push(function () {
return source.getAttachment(id, name);
})
.push(function (blob) {
return source.putAttachment(post_id, name, blob);
});
}
for (key in attachment_dict) { for (key in attachment_dict) {
if (attachment_dict.hasOwnProperty(key)) { if (attachment_dict.hasOwnProperty(key)) {
copyAttachment(key); copyAttachment(key);
}
} }
return copy_queue; }
}) return copy_queue;
.push(function () { })
return source.remove(id); .push(function () {
}) return source.remove(id);
.push(function () { })
return context._signature_sub_storage.remove(id); .push(function () {
}) return context._signature_sub_storage.remove(id);
.push(function () { })
to_skip = true; .push(function () {
return context._signature_sub_storage.put(post_id, { to_skip = true;
"hash": hash return context._signature_sub_storage.put(post_id, {
}); hash: hash,
}) from_local: from_local
.push(function () {
skip_document_dict[post_id] = null;
});
} else {
result = destination.put(id, doc)
.push(function () {
return context._signature_sub_storage.put(id, {
"hash": hash
});
}); });
} })
return result .push(function () {
skip_document_dict[post_id] = null;
});
} else {
result
.push(function () { .push(function () {
if (to_skip) { // Drop signature if the destination document was empty
skip_document_dict[id] = null; // 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 () {
return context._signature_sub_storage.put(id, {
hash: hash,
from_local: from_local
});
}); });
} }
return result
.push(function () {
if (to_skip) {
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,
// Do not delete a document if it has an attachment skip_deleted_document_dict) {
// ie, replication should prevent losing user data // Do not delete a document if it has an attachment
// Synchronize attachments before, to ensure // ie, replication should prevent losing user data
// all of them will be deleted too // Synchronize attachments before, to ensure
return repairDocumentAttachment(id) // all of them will be deleted too
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,292 +9377,370 @@ return new Parser; ...@@ -9026,292 +9377,370 @@ return new Parser;
return; return;
} }
throw error; throw error;
})
.push(function () {
skip_document_dict[id] = null;
}); });
} }
return result
.push(function () {
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,
source, destination, id, skip_deleted_document_dict,
conflict_force, conflict_revert, cache, destination_key,
conflict_ignore, status_hash, local_hash, doc,
options) { source, destination, id,
return destination.get(id) conflict_force, conflict_revert,
.push(function (remote_doc) { conflict_ignore,
return [remote_doc, generateHash(stringify(remote_doc))]; options) {
}, function (error) { var from_local = options.from_local;
if ((error instanceof jIO.util.jIOError) && return new RSVP.Queue()
(error.status_code === 404)) { .push(function () {
return [null, null]; if (options.signature_hash_key !== undefined) {
} return callAllDocsOnStorage(context, destination,
throw error; cache, destination_key)
}) .push(function (result) {
.push(function (remote_list) { if (result.hasOwnProperty(id)) {
var remote_doc = remote_list[0], return [null, result[id][options.signature_hash_key]];
remote_hash = remote_list[1]; }
return [null, null];
if (local_hash === remote_hash) { });
// Same modifications on both side }
if (local_hash === null) { return destination.get(id)
// Deleted on both side, drop signature .push(function (remote_doc) {
return context._signature_sub_storage.remove(id) return [remote_doc, generateHash(stringify(remote_doc))];
.push(function () { }, function (error) {
skip_document_dict[id] = null; if ((error instanceof jIO.util.jIOError) &&
}); (error.status_code === 404)) {
return [null, null];
} }
throw error;
});
})
return context._signature_sub_storage.put(id, { .push(function (remote_list) {
"hash": local_hash var remote_doc = remote_list[0],
}) remote_hash = remote_list[1];
if (local_hash === remote_hash) {
// Same modifications on both side
if (local_hash === null) {
// Deleted on both side, drop signature
return context._signature_sub_storage.remove(id)
.push(function () { .push(function () {
skip_document_dict[id] = null; skip_document_dict[id] = null;
}); });
} }
if ((remote_hash === status_hash) || (conflict_force === true)) { return context._signature_sub_storage.put(id, {
// Modified only locally. No conflict or force hash: local_hash,
if (local_hash === null) { from_local: from_local
// Deleted locally })
return propagateDeletion(destination, id); .push(function () {
} skip_document_dict[id] = null;
return propagateModification(source, destination, doc, });
local_hash, id, }
{use_post: ((options.use_post) &&
(remote_hash === null))});
}
// Conflict cases
if (conflict_ignore === true) {
return;
}
if ((conflict_revert === true) || (local_hash === null)) { if ((remote_hash === status_hash) || (conflict_force === true)) {
// Automatically resolve conflict or force revert // Modified only locally. No conflict or force
if (remote_hash === null) { if (local_hash === null) {
// Deleted remotely // Deleted locally
return propagateDeletion(source, id); return propagateDeletion(context, destination, id,
} skip_document_dict,
return propagateModification( skip_deleted_document_dict);
destination,
source,
remote_doc,
remote_hash,
id,
{use_post: ((options.use_revert_post) &&
(local_hash === null))}
);
} }
return propagateModification(context, source, destination, doc,
local_hash, id, skip_document_dict,
skip_deleted_document_dict,
{use_post: ((options.use_post) &&
(remote_hash === null)),
from_local: from_local,
create_new_document:
((remote_hash === null) &&
(status_hash !== null))
});
}
// Conflict cases
if (conflict_ignore === true) {
return;
}
// Minimize conflict if it can be resolved if ((conflict_revert === true) || (local_hash === null)) {
// Automatically resolve conflict or force revert
if (remote_hash === null) { if (remote_hash === null) {
// Copy remote modification remotely // Deleted remotely
return propagateModification(source, destination, doc, return propagateDeletion(context, source, id, skip_document_dict,
local_hash, id, skip_deleted_document_dict);
{use_post: options.use_post});
} }
throw new jIO.util.jIOError("Conflict on '" + id + "': " + return propagateModification(
stringify(doc || '') + " !== " + context,
stringify(remote_doc || ''), destination,
409); source,
}); remote_doc,
} remote_hash,
id,
skip_document_dict,
skip_deleted_document_dict,
{use_post: ((options.use_revert_post) &&
(local_hash === null)),
from_local: !from_local,
create_new_document: ((local_hash === null) &&
(status_hash !== null))}
);
}
// Minimize conflict if it can be resolved
if (remote_hash === null) {
// Copy remote modification remotely
return propagateModification(context, source, destination, doc,
local_hash, id, skip_document_dict,
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 + "': " +
stringify(doc) + " !== " +
stringify(remote_doc),
409);
});
}
function checkLocalDeletion(queue, context, skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
destination, id, source,
conflict_force, conflict_revert,
conflict_ignore, options) {
var status_hash;
queue
.push(function () {
return context._signature_sub_storage.get(id);
})
.push(function (result) {
status_hash = result.hash;
return checkAndPropagate(context, skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
status_hash, null, null,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
});
}
function checkSignatureDifference(queue, context, skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
local_hash, status_hash,
options) {
queue
.push(function () {
if (local_hash === null) {
// Hash was not provided by the allDocs query
return source.get(id);
}
return null;
})
.push(function (doc) {
if (local_hash === null) {
// Hash was not provided by the allDocs query
local_hash = generateHash(stringify(doc));
}
function checkLocalDeletion(queue, destination, id, source, if (local_hash !== status_hash) {
conflict_force, conflict_revert, return checkAndPropagate(context, skip_document_dict,
conflict_ignore, options) { skip_deleted_document_dict,
var status_hash; cache, destination_key,
queue status_hash, local_hash, doc,
.push(function () {
return context._signature_sub_storage.get(id);
})
.push(function (result) {
status_hash = result.hash;
return checkAndPropagate(status_hash, null, null,
source, destination, id, source, destination, id,
conflict_force, conflict_revert, conflict_force, conflict_revert,
conflict_ignore, conflict_ignore,
options); options);
}); }
} });
}
function checkSignatureDifference(queue, source, destination, id, function pushStorage(context, skip_document_dict,
conflict_force, conflict_revert, skip_deleted_document_dict,
conflict_ignore, cache, source_key, destination_key,
is_creation, is_modification, source, destination, signature_allDocs, options) {
getMethod, options) { var argument_list = [],
queue argument_list_deletion = [];
.push(function () { if (!options.hasOwnProperty("use_post")) {
// Optimisation to save a get call to signature storage options.use_post = false;
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,
conflict_force, conflict_revert,
conflict_ignore,
options);
}
});
} }
if (!options.hasOwnProperty("use_revert_post")) {
function checkBulkSignatureDifference(queue, source, destination, id_list, options.use_revert_post = false;
document_status_list, options,
conflict_force, conflict_revert,
conflict_ignore) {
queue
.push(function () {
return source.bulk(id_list);
})
.push(function (result_list) {
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];
};
}
for (i = 0; i < result_list.length; i += 1) {
checkSignatureDifference(sub_queue, source, destination,
id_list[i].parameter_list[0],
conflict_force, conflict_revert,
conflict_ignore,
document_status_list[i].is_creation,
document_status_list[i].is_modification,
getResult(i), options);
}
return sub_queue;
});
} }
return callAllDocsOnStorage(context, source, cache, source_key)
function pushStorage(source, destination, options) { .push(function (source_allDocs) {
var queue = new RSVP.Queue(); var i,
if (!options.hasOwnProperty("use_post")) { local_dict = {},
options.use_post = false; signature_dict = {},
} is_modification,
if (!options.hasOwnProperty("use_revert_post")) { is_creation,
options.use_revert_post = false; status_hash,
} local_hash,
return queue key,
.push(function () { queue = new RSVP.Queue();
return RSVP.all([ for (key in source_allDocs) {
source.allDocs(context._query_options), if (source_allDocs.hasOwnProperty(key)) {
context._signature_sub_storage.allDocs() if (!skip_document_dict.hasOwnProperty(key)) {
]); local_dict[key] = source_allDocs[key];
})
.push(function (result_list) {
var i,
local_dict = {},
document_list = [],
document_status_list = [],
signature_dict = {},
is_modification,
is_creation,
key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
result_list[0].data.rows[i].id
)) {
local_dict[result_list[0].data.rows[i].id] = i;
} }
} }
for (i = 0; i < result_list[1].data.total_rows; i += 1) { }
if (!skip_document_dict.hasOwnProperty( /*
result_list[1].data.rows[i].id for (i = 0; i < source_allDocs.data.total_rows; i += 1) {
)) { if (!skip_document_dict.hasOwnProperty(
signature_dict[result_list[1].data.rows[i].id] = i; source_allDocs.data.rows[i].id
} )) {
local_dict[source_allDocs.data.rows[i].id] =
source_allDocs.data.rows[i].value;
} }
for (key in local_dict) { }
if (local_dict.hasOwnProperty(key)) { */
is_modification = signature_dict.hasOwnProperty(key) for (i = 0; i < signature_allDocs.data.total_rows; i += 1) {
&& options.check_modification; if (!skip_document_dict.hasOwnProperty(
is_creation = !signature_dict.hasOwnProperty(key) signature_allDocs.data.rows[i].id
&& options.check_creation; )) {
if (is_modification === true || is_creation === true) { signature_dict[signature_allDocs.data.rows[i].id] =
if (options.use_bulk_get === true) { signature_allDocs.data.rows[i].value.hash;
document_list.push({ }
method: "get", }
parameter_list: [key] for (key in local_dict) {
}); if (local_dict.hasOwnProperty(key)) {
document_status_list.push({ is_modification = signature_dict.hasOwnProperty(key)
is_creation: is_creation, && options.check_modification;
is_modification: is_modification is_creation = !signature_dict.hasOwnProperty(key)
}); && options.check_creation;
} else {
checkSignatureDifference(queue, source, destination, key, if (is_creation === true) {
options.conflict_force, status_hash = null;
options.conflict_revert, } else if (is_modification === true) {
options.conflict_ignore, status_hash = signature_dict[key];
is_creation, is_modification, }
source.get.bind(source),
options); 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) {
argument_list.push([undefined, context, skip_document_dict,
skip_deleted_document_dict,
cache, destination_key,
source, destination,
key,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
local_hash, status_hash,
options]);
}
} }
if (options.check_deletion === true) { }
for (key in signature_dict) { queue
if (signature_dict.hasOwnProperty(key)) { .push(function () {
if (!local_dict.hasOwnProperty(key)) { return dispatchQueue(
checkLocalDeletion(queue, destination, key, source, context,
options.conflict_force, checkSignatureDifference,
options.conflict_revert, argument_list,
options.conflict_ignore, options.operation_amount
options); );
} });
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (!local_dict.hasOwnProperty(key)) {
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_revert,
options.conflict_ignore,
options]);
} else {
skip_deleted_document_dict[key] = null;
} }
} }
} }
if ((options.use_bulk_get === true) && (document_list.length !== 0)) { }
checkBulkSignatureDifference(queue, source, destination, if (argument_list_deletion.length !== 0) {
document_list, document_status_list, queue.push(function () {
options, return dispatchQueue(
options.conflict_force, context,
options.conflict_revert, checkLocalDeletion,
options.conflict_ignore); argument_list_deletion,
} 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) {
context._signature_hash // 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
);
}
}) })
.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
context._signature_hash, .__storage._sub_storage.put(
{} context._signature_hash,
); {}
);
} }
throw error; throw error;
}) })
...@@ -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 () {
return signature_allDocs;
}); });
} }
return signature_allDocs;
}) })
.push(function () { .push(function (signature_allDocs) {
// Autoactivate bulk if substorage implements it
// 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;
}
}
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,
function repairDocument(id) { len = result.data.total_rows;
repair_document_queue
.push(function () { for (i = 0; i < len; i += 1) {
return repairDocumentAttachment(id); row = result.data.rows[i];
}); // Do not synchronize attachment if one version of the document
} // is deleted but not pushed to the other storage
if (!skip_deleted_document_dict.hasOwnProperty(row.id)) {
for (i = 0; i < result.data.total_rows; i += 1) { local_argument_list.push(
repairDocument(result.data.rows[i].id); [undefined, context, row.id, context._signature_hash_key,
row.value.hash, row.value.attachment_hash,
row.value.from_local]
);
}
} }
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,35 +12852,30 @@ return new Parser; ...@@ -12442,35 +12852,30 @@ return new Parser;
return tx; return tx;
} }
function handleCursor(request, callback) { function handleCursor(request, callback, resolve, reject) {
function resolver(resolve, reject) { request.onerror = function (error) {
// Open DB // if (request.transaction) {
request.onerror = function (error) { request.transaction.abort();
if (request.transaction) { }
request.transaction.abort(); reject(error);
} };
reject(error);
};
request.onsuccess = function (evt) {
var cursor = evt.target.result;
if (cursor) {
// XXX Wait for result
try {
callback(cursor);
} catch (error) {
reject(error);
}
// continue to next iteration request.onsuccess = function (evt) {
cursor["continue"](); var cursor = evt.target.result;
} else { if (cursor) {
resolve(); // XXX Wait for result
try {
callback(cursor);
} catch (error) {
reject(error);
} }
};
} // continue to next iteration
// XXX Canceller??? cursor["continue"]();
return new RSVP.Promise(resolver); } else {
resolve();
}
};
} }
IndexedDBStorage.prototype.buildQuery = function (options) { IndexedDBStorage.prototype.buildQuery = function (options) {
...@@ -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) {
var tx = openTransaction(db, ["metadata"], "readonly"); return new RSVP.Promise(function (resolve, reject) {
if (options.include_docs === true) { var tx = openTransaction(db, ["metadata"], "readonly");
return handleCursor(tx.objectStore("metadata").index("_id") if (options.include_docs === true) {
.openCursor(), pushIncludedMetadata); handleCursor(tx.objectStore("metadata").index("_id").openCursor(),
} pushIncludedMetadata, resolve, reject);
return handleCursor(tx.objectStore("metadata").index("_id") } else {
.openKeyCursor(), pushMetadata); handleCursor(tx.objectStore("metadata").index("_id")
.openKeyCursor(), pushMetadata, resolve, reject);
}
});
}) })
.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 {
// XXX How to get ID reject(new jIO.util.jIOError(
reject(new jIO.util.jIOError("Cannot find document", 404)); "IndexedDB: cannot find object '" + id + "' in the '" +
}; store.name + "' store",
} 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) {
var transaction = openTransaction(db, ["metadata", "attachment"], return new RSVP.Promise(function (resolve, reject) {
"readonly"); var transaction = openTransaction(db, ["metadata", "attachment"],
return RSVP.all([ "readonly");
handleGet(transaction.objectStore("metadata").get(id)), function getAttachments() {
handleCursor(transaction.objectStore("attachment").index("_id") handleCursor(
.openCursor(IDBKeyRange.only(id)), addEntry) transaction.objectStore("attachment").index("_id")
]); .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) {
var transaction = openTransaction(db, ["metadata"], "readwrite"); return new RSVP.Promise(function (resolve, reject) {
return handleRequest(transaction.objectStore("metadata").put({ var transaction = openTransaction(db, ["metadata"], "readwrite");
"_id": id, handleRequest(
"doc": metadata transaction.objectStore("metadata").put({
})); "_id": id,
"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) {
var transaction = openTransaction(db, ["metadata", "attachment", return new RSVP.Promise(function (resolve, reject) {
"blob"], "readwrite"); function resolver() {
return RSVP.all([ if (resolved_amount < 2) {
handleRequest(transaction resolved_amount += 1;
.objectStore("metadata")["delete"](id)), } else {
resolve();
}
}
var transaction = openTransaction(db, ["metadata", "attachment",
"blob"], "readwrite");
handleRequest(
transaction.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) {
var total_length = attachment.info.length,
i,
promise_list = [],
store = transaction.objectStore("blob"),
start_index,
end_index;
type = attachment.info.content_type;
start = options.start || 0;
end = options.end || total_length;
if (end > total_length) {
end = total_length;
}
if (start < 0 || end < 0) {
throw new jIO.util.jIOError("_start and _end must be positive",
400);
}
if (start > end) {
throw new jIO.util.jIOError("_start is greater than _end",
400);
}
start_index = Math.floor(start / UNITE);
end_index = Math.floor(end / UNITE);
if (end % UNITE === 0) {
end_index -= 1;
}
for (i = start_index; i <= end_index; i += 1) {
promise_list.push(
handleGet(store.get(buildKeyPath([id,
name, i])))
); );
} function getBlob(attachment) {
return RSVP.all(promise_list); var total_length = attachment.info.length,
result_list = [],
store = transaction.objectStore("blob"),
start_index,
end_index;
type = attachment.info.content_type;
start = options.start || 0;
end = options.end || total_length;
if (end > total_length) {
end = total_length;
}
if (start < 0 || end < 0) {
throw new jIO.util.jIOError(
"_start and _end must be positive",
400
);
}
if (start > end) {
throw new jIO.util.jIOError("_start is greater than _end",
400);
}
start_index = Math.floor(start / UNITE);
end_index = Math.floor(end / UNITE) - 1;
if (end % UNITE === 0) {
end_index -= 1;
}
function resolver(result) {
result_list.push(result);
resolve(result_list);
}
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
);
};
}
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,38 +13179,43 @@ return new Parser; ...@@ -12708,38 +13179,43 @@ 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) {
"_key_path": buildKeyPath([id, name]), return function () {
"_id": id, i += 1;
"_attachment": name, handleRequest(
"info": { blob_store.put({
"content_type": blob.type, "_key_path": buildKeyPath([id, name, i]),
"length": blob.size "_id" : id,
} "_attachment" : name,
})) "_part" : i,
], "blob": blob_part[i]
len = blob_part.length, }),
blob_store = transaction.objectStore("blob"), (i < len) ? putBlobPart(i) : resolve,
i; reject
for (i = 0; i < len; i += 1) { );
promise_list.push( };
handleRequest(blob_store.put({ }
"_key_path": buildKeyPath([id, name, handleRequest(
i]), attachment_store.put({
"_id" : id, "_key_path": buildKeyPath([id, name]),
"_attachment" : name, "_id": id,
"_part" : i, "_attachment": name,
"blob": blob_part[i] "info": {
})) "content_type": blob.type,
); "length": blob.size
} }
// Store all new data }),
return RSVP.all(promise_list); putBlobPart(-1),
reject
);
}
removeAttachment(transaction, id, name, write, reject);
});
}); });
}; };
...@@ -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,379 +2030,385 @@ if (typeof document.contains !== 'function') { ...@@ -1989,379 +2030,385 @@ 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
// is triggered before everything was ready.
// (For instance, the HTML-tag for the self gadget gets inserted after
// page load)
renderJS.manualBootstrap = function () {
all_dependency_loaded_deferred.resolve();
};
document.addEventListener('DOMContentLoaded',
all_dependency_loaded_deferred.resolve, false);
function configureMutationObserver(TmpConstructor, url, root_gadget) {
// XXX HTML properties can only be set when the DOM is fully loaded
var settings = renderJS.parseGadgetHTMLDocument(document, url),
j,
key,
fragment = document.createDocumentFragment();
for (key in settings) {
if (settings.hasOwnProperty(key)) {
TmpConstructor.prototype['__' + key] = settings[key];
}
}
TmpConstructor.__template_element = document.createElement("div");
root_gadget.element = document.body;
root_gadget.state = {};
for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
fragment.appendChild(
root_gadget.element.childNodes[j].cloneNode(true)
);
}
TmpConstructor.__template_element.appendChild(fragment);
return RSVP.all([root_gadget.getRequiredJSList(),
root_gadget.getRequiredCSSList()])
.then(function (all_list) {
var i,
js_list = all_list[0],
css_list = all_list[1];
for (i = 0; i < js_list.length; i += 1) {
javascript_registration_dict[js_list[i]] = null;
}
for (i = 0; i < css_list.length; i += 1) {
stylesheet_registration_dict[css_list[i]] = null;
}
gadget_loading_klass_list.shift();
}).then(function () {
// select the target node
var target = document.querySelector('body'),
// create an observer instance
observer = new MutationObserver(function (mutations) {
var i, k, len, len2, node, added_list;
mutations.forEach(function (mutation) {
if (mutation.type === 'childList') {
len = mutation.removedNodes.length;
for (i = 0; i < len; i += 1) {
node = mutation.removedNodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.hasAttribute("data-gadget-url") &&
(node._gadget !== undefined)) {
createMonitor(node._gadget);
}
added_list =
node.querySelectorAll("[data-gadget-url]");
len2 = added_list.length;
for (k = 0; k < len2; k += 1) {
node = added_list[k];
if (node._gadget !== undefined) {
createMonitor(node._gadget);
}
}
}
}
len = mutation.addedNodes.length;
for (i = 0; i < len; i += 1) {
node = mutation.addedNodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.hasAttribute("data-gadget-url") &&
(node._gadget !== undefined)) {
if (document.contains(node)) {
startService(node._gadget);
}
}
added_list =
node.querySelectorAll("[data-gadget-url]");
len2 = added_list.length;
for (k = 0; k < len2; k += 1) {
node = added_list[k];
if (document.contains(node)) {
if (node._gadget !== undefined) {
startService(node._gadget);
}
}
}
}
}
}
});
}),
// configuration of the observer:
config = {
childList: true,
subtree: true,
attributes: false,
characterData: false
};
// pass in the target node, as well as the observer options
observer.observe(target, config);
return root_gadget;
});
}
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, root_gadget,
loading_gadget_promise = new RSVP.Queue(),
declare_method_count = 0,
embedded_channel, embedded_channel,
notifyReady,
notifyDeclareMethod, notifyDeclareMethod,
gadget_ready = false, declare_method_list_waiting,
iframe_top_gadget, loading_result,
last_acquisition_gadget, channel_defer,
declare_method_list_waiting = [], real_result_list;
gadget_failed = false, // gadget_failed = false,
gadget_error, // connection_ready = false;
connection_ready = false;
// Create the gadget class for the current url // Create the gadget class for the current url
if (gadget_model_defer_dict.hasOwnProperty(url)) { if (gadget_model_defer_dict.hasOwnProperty(url)) {
throw new Error("bootstrap should not be called twice"); 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); // 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);
};
} else { real_result_list = [TmpConstructor, root_gadget, embedded_channel,
// Create the root gadget instance and put it in the loading stack declare_method_list_waiting];
TmpConstructor = RenderJSEmbeddedGadget; if (window.self === window.top) {
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice(); loading_result = real_result_list;
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice(); } else {
TmpConstructor.prototype.__path = url; channel_defer = RSVP.defer();
root_gadget = new RenderJSEmbeddedGadget(); loading_result = RSVP.any([
setAqParent(root_gadget, last_acquisition_gadget); channel_defer.promise,
new RSVP.Queue()
// Create the communication channel .push(function () {
embedded_channel = Channel.build({ // Expect the channel to parent to be usable after 1 second
window: window.parent, // If not, consider the gadget as the root
origin: "*", // Drop all iframe channel communication
scope: "renderJS", return RSVP.delay(1000);
onReady: function () { })
var k; .push(function () {
iframe_top_gadget = false; real_result_list[2] = undefined;
//Default: Define __aq_parent to inform parent window return real_result_list;
root_gadget.__aq_parent = })
TmpConstructor.prototype.__aq_parent = function (method_name, ]);
argument_list, time_out) { // Create the communication channel
return new RSVP.Promise(function (resolve, reject) { embedded_channel = Channel.build({
embedded_channel.call({ window: window.parent,
method: "acquire", origin: "*",
params: [ scope: "renderJS",
method_name, onReady: function () {
argument_list var k,
], len;
success: function (s) { // Channel is ready, so now declare all methods
resolve(s); notifyDeclareMethod = function (name) {
}, declare_method_list_waiting.push(
error: function (e) { new RSVP.Promise(function (resolve, reject) {
reject(e); embedded_channel.call({
}, method: "declareMethod",
timeout: time_out params: name,
}); success: resolve,
}); error: reject
};
// 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) {
notifyDeclareMethod(declare_method_list_waiting[k]);
}
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 len = declare_method_list_waiting.length;
notifyReady = function () { for (k = 0; k < len; k += 1) {
if ((declare_method_count === 0) && (gadget_ready === true)) { notifyDeclareMethod(declare_method_list_waiting[k]);
embedded_channel.notify({method: "ready"});
} }
};
// Inform parent gadget about declareMethod calls here. channel_defer.resolve(real_result_list);
notifyDeclareMethod = function (name) { }
declare_method_list_waiting.push(name); });
}; real_result_list[2] = embedded_channel;
}
notifyDeclareMethod("getInterfaceList"); // Surcharge declareMethod to inform parent window
notifyDeclareMethod("getRequiredCSSList"); TmpConstructor.declareMethod = function (name, callback) {
notifyDeclareMethod("getRequiredJSList"); var result = RenderJSGadget.declareMethod.apply(
notifyDeclareMethod("getPath"); this,
notifyDeclareMethod("getTitle"); [name, callback]
);
// Surcharge declareMethod to inform parent window notifyDeclareMethod(name);
TmpConstructor.declareMethod = function (name, callback) { return result;
var result = RenderJSGadget.declareMethod.apply( };
this,
[name, callback]
);
notifyDeclareMethod(name);
return result;
};
TmpConstructor.declareService = TmpConstructor.declareService =
RenderJSGadget.declareService; RenderJSGadget.declareService;
TmpConstructor.declareJob = TmpConstructor.declareJob =
RenderJSGadget.declareJob; RenderJSGadget.declareJob;
TmpConstructor.onEvent = TmpConstructor.onEvent =
RenderJSGadget.onEvent; RenderJSGadget.onEvent;
TmpConstructor.declareAcquiredMethod = TmpConstructor.onLoop =
RenderJSGadget.declareAcquiredMethod; RenderJSGadget.onLoop;
TmpConstructor.allowPublicAcquisition = TmpConstructor.declareAcquiredMethod =
RenderJSGadget.allowPublicAcquisition; RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
iframe_top_gadget = true; RenderJSGadget.allowPublicAcquisition;
}
TmpConstructor.prototype.__acquired_method_dict = {}; TmpConstructor.prototype.__acquired_method_dict = {};
gadget_loading_klass_list.push(TmpConstructor); gadget_loading_klass_list.push(TmpConstructor);
function init() { return loading_result;
// XXX HTML properties can only be set when the DOM is fully loaded }
var settings = renderJS.parseGadgetHTMLDocument(document, url),
j,
key,
fragment = document.createDocumentFragment();
for (key in settings) {
if (settings.hasOwnProperty(key)) {
TmpConstructor.prototype['__' + key] = settings[key];
}
}
TmpConstructor.__template_element = document.createElement("div");
root_gadget.element = document.body;
root_gadget.state = {};
for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
fragment.appendChild(
root_gadget.element.childNodes[j].cloneNode(true)
);
}
TmpConstructor.__template_element.appendChild(fragment);
RSVP.all([root_gadget.getRequiredJSList(),
root_gadget.getRequiredCSSList()])
.then(function (all_list) {
var i,
js_list = all_list[0],
css_list = all_list[1];
for (i = 0; i < js_list.length; i += 1) {
javascript_registration_dict[js_list[i]] = null;
}
for (i = 0; i < css_list.length; i += 1) {
stylesheet_registration_dict[css_list[i]] = null;
}
gadget_loading_klass_list.shift();
}).then(function () {
// select the target node
var target = document.querySelector('body'),
// create an observer instance
observer = new MutationObserver(function (mutations) {
var i, k, len, len2, node, added_list;
mutations.forEach(function (mutation) {
if (mutation.type === 'childList') {
len = mutation.removedNodes.length;
for (i = 0; i < len; i += 1) {
node = mutation.removedNodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.hasAttribute("data-gadget-url") &&
(node._gadget !== undefined)) {
createMonitor(node._gadget);
}
added_list =
node.querySelectorAll("[data-gadget-url]");
len2 = added_list.length;
for (k = 0; k < len2; k += 1) {
node = added_list[k];
if (node._gadget !== undefined) {
createMonitor(node._gadget);
}
}
}
}
len = mutation.addedNodes.length; function triggerReadyList(TmpConstructor, root_gadget) {
for (i = 0; i < len; i += 1) { // XXX Probably duplicated
node = mutation.addedNodes[i]; var i,
if (node.nodeType === Node.ELEMENT_NODE) { ready_queue = new RSVP.Queue();
if (node.hasAttribute("data-gadget-url") &&
(node._gadget !== undefined)) {
if (document.contains(node)) {
startService(node._gadget);
}
}
added_list =
node.querySelectorAll("[data-gadget-url]");
len2 = added_list.length;
for (k = 0; k < len2; k += 1) {
node = added_list[k];
if (document.contains(node)) {
if (node._gadget !== undefined) {
startService(node._gadget);
}
}
}
}
}
} function ready_wrapper() {
}); return root_gadget;
}), }
// configuration of the observer: function ready_executable_wrapper(fct) {
config = { return function (g) {
childList: true, return fct.call(g, g);
subtree: true, };
attributes: false, }
characterData: false TmpConstructor.ready(function () {
}; return startService(this);
});
// pass in the target node, as well as the observer options
observer.observe(target, config); ready_queue.push(ready_wrapper);
for (i = 0; i < TmpConstructor.__ready_list.length; i += 1) {
return root_gadget; // Put a timeout?
}).then(resolve, function (e) { ready_queue
reject(e); .push(ready_executable_wrapper(TmpConstructor.__ready_list[i]))
console.error(e); // Always return the gadget instance after ready function
throw e; .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
}); });
} });
document.addEventListener('DOMContentLoaded', init, false); };
// bind calls to renderJS method on the instance
embedded_channel.bind("methodCall", function (trans, v) {
root_gadget[v[0]].apply(root_gadget, v[1])
.push(function (g) {
trans.complete(g);
}, function (e) {
trans.error(e.toString());
});
trans.delayReturn(true);
}); });
}
loading_gadget_promise 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 () { .push(function () {
return loading_klass_promise; // Wait for the loading gadget to be created
return wait_for_gadget_loaded;
}) })
.push(function (root_gadget) { .push(function (result_list) {
var i; TmpConstructor = result_list[0];
root_gadget = result_list[1];
function ready_wrapper() { embedded_channel = result_list[2];
return root_gadget; declare_method_list_waiting = result_list[3];
// Wait for all the gadget dependencies to be loaded
return all_dependency_loaded_deferred.promise;
})
.push(function () {
// Wait for all methods to be correctly declared
return RSVP.all(declare_method_list_waiting);
})
.push(function (result_list) {
if (embedded_channel !== undefined) {
finishAqParentConfiguration(TmpConstructor, root_gadget,
embedded_channel);
} }
function ready_executable_wrapper(fct) { // Check all DOM modifications to correctly start/stop services
return function (g) { return configureMutationObserver(TmpConstructor, url, root_gadget);
return fct.call(g, g); })
}; .push(function () {
// Trigger all ready functions
return triggerReadyList(TmpConstructor, root_gadget);
})
.push(function () {
if (embedded_channel !== undefined) {
embedded_channel.notify({method: "ready"});
} }
TmpConstructor.ready(function () { })
return startService(this); .push(undefined, function (e) {
}); letsCrash(e);
if (embedded_channel !== undefined) {
loading_gadget_promise.push(ready_wrapper); embedded_channel.notify({method: "failed", params: e.toString()});
for (i = 0; i < TmpConstructor.__ready_list.length; i += 1) {
// Put a timeout?
loading_gadget_promise
.push(ready_executable_wrapper(TmpConstructor.__ready_list[i]))
// Always return the gadget instance after ready function
.push(ready_wrapper);
} }
throw e;
}); });
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
.then(function () {
gadget_ready = true;
if (connection_ready) {
notifyReady();
}
})
.fail(function (e) {
//top gadget in iframe
if (iframe_top_gadget) {
gadget_failed = true;
gadget_error = e.toString();
letsCrash(e);
} else {
embedded_channel.notify({method: "failed", params: e.toString()});
}
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
...@@ -1023,4 +1023,4 @@ define("rsvp", ...@@ -1023,4 +1023,4 @@ define("rsvp",
__exports__.reject = reject; __exports__.reject = reject;
}); });
window.RSVP = requireModule("rsvp"); window.RSVP = requireModule("rsvp");
})(window); })(window);
\ 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