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