mustache_templates-8.x-1.0-beta4/js/sync.js
js/sync.js
/**
* @file
* DOM content synchronization with Mustache templates.
*
* Using Mustache.js by
* Copyright (c) 2009 Chris Wanstrath (Ruby)
* Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
* Copyright (c) 2010-2015 The mustache.js community
* Using morphdom by
* Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
*/
window.mustacheSync = mustacheSync || {items: [], templates: []};
(function (Drupal, sync, Mustache) {
sync.registry = sync.registry || {
items: [],
pending: [],
templates: {},
providers: [],
listeners: {},
evald: {},
magic: {}
};
sync.___internals = sync.___internals || {
subset: function (data, select) {
var subset = data;
var slength = select.length;
var skey;
var i;
for (i = 0; i < slength; i++) {
skey = select[i];
if (subset.hasOwnProperty(skey)) {
subset = subset[skey];
}
else {
return false;
}
}
return subset;
},
isEmpty: function (obj) {
var k;
if (!(typeof obj === 'object') || obj === null) {
return !obj;
}
if (Array.isArray(obj)) {
return !obj.length;
}
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
},
trigger: function (target, type, canBubble, cancelable, detail) {
var event = document.createEvent('CustomEvent');
if (typeof detail === 'undefined') {
detail = null;
}
event.initCustomEvent(type, canBubble, cancelable, detail);
target.dispatchEvent(event);
},
item: {
update: function (period, force_fetch, no_repeat) {
var provider = this.provider;
var targetElement = this.element || null;
var internals = sync.___internals;
var fetch = internals.item.fetch;
var render = internals.item.render;
var trigger = internals.trigger;
var buffer = {size: 0};
if (typeof period === 'undefined') {
period = this.period;
}
if (no_repeat !== true) {
no_repeat = false;
}
if (targetElement) {
targetElement.classList.add('syncing');
targetElement.classList.remove('synced');
trigger(targetElement, 'mustacheSyncBegin', true, false, this);
}
if (provider) {
if (provider.faulty === true) {
if (period > 0) {
// Retry when period is set.
this.sync(this.delay + 1000, period, false, true, no_repeat);
}
return;
}
if (provider.fetching === true) {
// Retry.
this.sync(this.delay + 10, period, false, true, no_repeat);
return;
}
if ((provider.fetched === null) || (force_fetch === true) || ((period > 0) && (period - 1 < Date.now() - provider.fetched)) || ((typeof this.max_age === 'number') && (this.max_age < Date.now() - provider.fetched))) {
fetch.call(this, period, no_repeat);
return;
}
}
render.call(this);
if (!no_repeat) {
// Repeat or sync next one.
buffer.next = this.next();
if (buffer.next) {
buffer.next.sync(period, period);
}
}
},
fetch: function (period, no_repeat) {
var provider = this.provider;
var fetch = sync.___internals.item.fetch;
var render = sync.___internals.item.render;
var isEmpty = sync.___internals.isEmpty;
var request = new XMLHttpRequest();
provider.fetching = true;
if (typeof period === 'undefined') {
period = this.period;
}
if (no_repeat !== true) {
no_repeat = false;
}
request.open('GET', provider.url, true);
request.onload = function () {
var data = false;
var next = false;
if (request.status === 304) {
data = !isEmpty(provider.latest) ? provider.latest : this.data || false;
provider.previousFetched = provider.fetched || null;
provider.fetched = Date.now();
}
else if (request.status >= 200 && request.status < 400) {
try {
data = JSON.parse(request.responseText);
provider.previous = provider.latest || null;
provider.previousFetched = provider.fetched || null;
provider.latest = data;
provider.fetched = Date.now();
}
catch (e) {
data = false;
}
}
else if (request.status >= 500) {
request.onerror();
return;
}
provider.fetching = false;
provider.faulty = false;
if (provider !== this.provider) {
return;
}
if (this.increment === null || !isEmpty(data)) {
this.previousData = this.data || this.previousData || false;
if (!(typeof data === 'object') || Array.isArray(data) || !data) {
this.data = data;
}
else if (typeof this.previousData === 'object' && this.previousData) {
this.data = Object.assign({}, this.previousData, data);
}
else {
this.data = Object.assign({}, data);
}
render.call(this);
}
if (!no_repeat) {
if (this.increment !== null && this.increment.loop && isEmpty(data)) {
next = this.next(0);
}
else {
next = this.next();
}
if (next) {
next.sync(period, period);
}
}
}.bind(this);
request.onerror = function () {
provider.fetching = false;
provider.faulty = true;
if (period > 0) {
// Retry when period is set.
setTimeout(fetch.bind(this, period, no_repeat), period + 5000);
}
}.bind(this);
request.send();
},
done: function () {
if (this.increment !== null) {
if (this.increment.i + 1 >= this.increment.max && !this.increment.loop) {
return true;
}
}
return this.limit === 0 || (this.period === 0 && this.limit < -1);
},
next: function (index) {
if (this.done()) {
return false;
}
if (this.increment !== null) {
if (typeof index === 'number') {
this.increment.i = index;
this.increment.value = this.increment.offset + (index * this.increment.step);
}
else if (this.increment.i + 1 >= this.increment.max) {
if (this.increment.loop) {
this.increment.i = 0;
this.increment.value = this.increment.offset;
}
}
else {
this.increment.i++;
this.increment.value += this.increment.step;
}
if (this.provider !== null) {
this.provider = this.provider.rebuild(this.increment.key, this.increment.value);
}
else {
this.data[this.increment.key] = this.increment.value;
}
}
return this;
},
prepare: function (rendered, buffer) {
return rendered;
},
finish: function (rendered, buffer) {
return rendered;
},
render: function () {
var rendered = false;
var buffer = {
template: sync.registry.templates[this.template],
data: this.data
};
var targetElement = this.element || null;
var adjacent = this.adjacent;
var internals = sync.___internals;
var subset = internals.subset;
var trigger = internals.trigger;
var prepare = internals.item.prepare;
var finish = internals.item.finish;
var magic = Object.assign({}, sync.registry.magic);
var execInner = this.eval;
var behaviors = targetElement && this.behaviors;
for (var k in magic) {
if (magic.hasOwnProperty(k)) {
magic[k] = magic[k].call(this, buffer);
}
}
if (this.select && buffer.data) {
buffer.data = subset(buffer.data, this.select);
}
if (buffer.data === false) {
if (targetElement) {
targetElement.classList.remove('syncing');
targetElement.classList.remove('synced');
targetElement.classList.add('not-synced');
targetElement.classList.add('error');
}
return false;
}
if (buffer.data === null) {
buffer.data = {};
}
if (typeof buffer.data === 'object') {
if (Array.isArray(buffer.data)) {
buffer.data = {___l: [Object.assign([], buffer.data)]};
buffer.template = '{{#___l}}' + buffer.template + '{{/___l}}'
}
buffer.data = Object.assign({}, buffer.data, magic);
if (this.previousData) {
buffer.data.previous = this.previousData;
delete buffer.data.previous.previous;
}
}
try {
rendered = prepare.call(this, rendered, buffer);
rendered = Mustache.render(buffer.template, buffer.data, sync.registry.templates);
rendered = finish.call(this, rendered, buffer);
}
catch (e) {
if (e.retry && e.component) {
for (buffer.ri = 0; buffer.ri < e.retry.length; buffer.ri++) {
buffer.retry = e.retry[buffer.ri];
buffer.retry.retries = buffer.retry.retries ? buffer.retry.retries + 1 : 1;
if (this === buffer.retry) {
setTimeout(sync.___internals.item.render.bind(this), buffer.retry.retries * 100);
}
else {
buffer.retry.sync(buffer.retry.retries * 100, 0, false, true, true);
}
}
return rendered;
}
if (e.abort) {
return rendered;
}
throw e;
}
behaviors && Drupal.detachBehaviors(targetElement);
if (this.morph || execInner) {
buffer.fragment = document.createElement('div');
buffer.fragment.innerHTML = rendered;
}
if (execInner) {
buffer.scripts = buffer.fragment.querySelectorAll('script');
buffer.eval = [];
for (buffer.i = 0; buffer.i < buffer.scripts.length; buffer.i++) {
buffer.script = buffer.scripts[buffer.i];
buffer.script.remove();
if (buffer.script.hasAttribute('src')) {
if (sync.registry.evald.hasOwnProperty(buffer.script.getAttribute('src'))) {
continue;
}
sync.registry.evald[buffer.script.getAttribute('src')] = true;
}
if (buffer.script.hasAttribute('id')) {
if (sync.registry.evald.hasOwnProperty(buffer.script.getAttribute('id'))) {
continue;
}
sync.registry.evald[buffer.script.getAttribute('id')] = true;
}
buffer.eval_script = document.createElement('script');
for (buffer.k = 0; buffer.k < buffer.script.attributes.length; buffer.k++) {
buffer.eval_script.setAttribute(buffer.script.attributes[buffer.k].name, buffer.script.attributes[buffer.k].value);
}
buffer.eval_script.innerHTML = buffer.script.innerHTML;
buffer.eval.push(buffer.eval_script);
}
rendered = buffer.fragment.innerHTML;
}
if (targetElement) {
if (this.morph) {
morphdom(targetElement, buffer.fragment, this.morph);
}
else {
switch (adjacent) {
case 'beforebegin':
case 'afterbegin':
case 'beforeend':
case 'afterend':
targetElement.insertAdjacentHTML(adjacent, rendered);
break;
default:
targetElement.innerHTML = rendered;
}
}
targetElement.classList.add('synced');
targetElement.classList.remove('syncing');
targetElement.classList.remove('not-synced');
targetElement.classList.remove('error');
}
if (execInner) {
for (buffer.i = 0; buffer.i < buffer.eval.length; buffer.i++) {
document.head.appendChild(buffer.eval[buffer.i]);
buffer.eval[buffer.i].remove();
}
}
buffer = null;
targetElement && trigger(targetElement, 'mustacheSyncFinish', true, false, this);
behaviors && Drupal.attachBehaviors(targetElement);
return rendered;
},
init: function () {
var internals = sync.___internals.item;
var getProvider = sync.___internals.provider.get;
if (this.initialized === true) {
// Already initialized, aborting.
return;
}
// Initialize the provider, if given.
if (typeof this.url === 'string') {
this.provider = getProvider(this.url);
this.data = this.data || {};
}
else if (typeof this.data === 'string') {
this.provider = getProvider(this.data);
this.data = {};
}
else {
this.data = this.data || {};
this.provider = null;
}
// Initialize the increment, if given.
this.increment = this.increment || null;
if (this.increment !== null) {
this.increment.offset = this.increment.offset || 0;
this.increment.key = this.increment.key || 'page';
this.increment.value = this.increment.offset;
this.increment.step = this.increment.step || 1;
this.increment.max = this.increment.max || -1;
this.increment.i = 0;
this.increment.loop = this.increment.loop || true;
if (this.provider !== null) {
this.provider = this.provider.rebuild(this.increment.key, this.increment.value);
}
else {
this.data[this.increment.key] = this.increment.value;
}
}
this.listen = this.listen || internals.listen.bind(this);
this.ready = this.ready || internals.ready.bind(this);
this.sync = this.sync || internals.sync.bind(this);
this.done = this.done || internals.done.bind(this);
this.next = this.next || internals.next.bind(this);
this.delay = this.delay || 0;
this.period = this.period || 0;
this.limit = this.limit || -1;
this.trigger = this.trigger || null;
this.adjacent = this.adjacent || null;
this.eval = (this.eval === true) || false;
this.behaviors = (this.behaviors === true) || (this.eval && this.behaviors !== false) || false;
if (this.adjacent) {
this.morph = false;
}
if (this.morph === true) {
this.morph = {childrenOnly: true};
}
this.started = false;
this.triggered = false;
this.initialized = true;
},
ready: function () {
if (!this.element && this.into) {
this.element = document.querySelector(this.into);
}
if (this.element === null) {
return false;
}
if (this.morph && !morphdom) {
// Morphing DOM trees require morphdom.
return false;
}
return sync.registry.templates.hasOwnProperty(this.template);
},
start: function () {
if (!this.ready() || this.started === true) {
return;
}
this.started = true;
if (this.trigger === null) {
// Synchronize immediately, or at least after a specified delay.
this.sync(this.delay, this.period);
}
else {
// Listen and synchronize by the specified triggers.
this.listen(this.trigger);
}
},
listen: function (triggers) {
var listeners = sync.registry.listeners;
var trigger;
var selector;
var event;
var limit;
var group;
var subscribers;
var i;
// Add this item as a subscriber for each specified triggering element.
for (i = 0; i < triggers.length; i++) {
trigger = triggers[i];
selector = trigger[0];
event = trigger[1];
limit = trigger[2];
if (!listeners.hasOwnProperty(selector)) {
listeners[selector] = {
elements: [],
events: {}
};
}
group = listeners[selector];
if (!group.events.hasOwnProperty(event)) {
group.events[event] = {
subscribers: []
};
group.events[event].triggered = function () {
var i;
var subscriber;
var item;
var length = this.subscribers.length;
for (i = 0; i < length; i++) {
subscriber = this.subscribers[i];
item = subscriber.item;
if (subscriber.limit === 0) {
continue;
}
subscriber.limit--;
if (item.triggered === true) {
if (item.limit < 0) {
item.limit = -1;
}
item = item.next();
if (item) {
subscriber.item = item;
item.sync(item.delay, 0, !item.max_age, false, true);
}
}
else {
item.triggered = true;
item.sync(item.delay, item.period, !item.max_age);
}
}
}.bind(group.events[event]);
}
subscribers = group.events[event].subscribers;
subscribers.push({item: this, limit: limit});
}
},
sync: function (delay, period, force_fetch, ignore_limit, no_repeat) {
if (!this.ready()) {
return;
}
if (ignore_limit !== true) {
if (this.done()) {
return;
}
this.limit--;
}
if (typeof delay === 'undefined') {
delay = this.delay;
}
if (typeof period === 'undefined') {
period = this.period;
}
if (force_fetch !== true) {
force_fetch = false;
}
if (no_repeat !== true) {
no_repeat = false;
}
setTimeout(sync.___internals.item.update.bind(this, period, force_fetch, no_repeat), delay);
}
},
provider: {
init: function (url) {
var internals = sync.___internals.provider;
var instance = {
url: url,
latest: {},
fetched: null,
fetching: false,
faulty: false
};
instance.getParts = internals.parts.bind(instance);
instance.getParams = internals.params.bind(instance);
instance.rebuild = internals.rebuild.bind(instance);
return instance;
},
get: function (url) {
var providers = sync.registry.providers;
var buffer = {size: providers.length};
for (buffer.i = 0; buffer.i < buffer.size; buffer.i++) {
if (providers[buffer.i].url === url) {
return providers[buffer.i];
}
}
while (buffer.size > 9) {
providers.shift();
buffer.size--;
}
buffer.provider = sync.___internals.provider.init(url);
providers.push(buffer.provider);
return buffer.provider;
},
rebuild: function (key, val) {
var getProvider = sync.___internals.provider.get;
var buffer = {isDifferent: false, params: {}, flat: []};
var newParams = key;
if (typeof key !== 'object') {
newParams = {};
newParams[key] = val;
}
this.getParams();
for (buffer.key in this.params) {
if (this.params.hasOwnProperty(buffer.key) && !newParams.hasOwnProperty(buffer.key)) {
buffer.val = this.params[buffer.key];
buffer.flat.push(buffer.key + '=' + buffer.val);
buffer.params[buffer.key] = buffer.val;
}
}
for (buffer.key in newParams) {
if (!newParams.hasOwnProperty(buffer.key)) {
continue;
}
buffer.val = newParams[buffer.key];
if (this.params.hasOwnProperty(buffer.key)) {
if ((typeof this.params[buffer.key] === 'string') && (typeof buffer.val === 'number')) {
this.params[buffer.key] = parseInt(this.params[buffer.key]);
}
if (this.params[buffer.key] !== buffer.val) {
buffer.isDifferent = true;
}
}
else {
buffer.isDifferent = true;
}
buffer.flat.push(buffer.key + '=' + buffer.val);
buffer.params[buffer.key] = buffer.val;
}
if (!buffer.isDifferent) {
return getProvider(this.url);
}
buffer.parts = document.createElement('a');
buffer.parts.href = this.url;
buffer.parts.search = buffer.flat.join('&');
buffer.provider = getProvider(buffer.parts.href);
buffer.provider.parts = buffer.parts;
buffer.provider.params = buffer.params;
return buffer.provider;
},
parts: function () {
if (!this.hasOwnProperty('parts')) {
// Extract and attach the url parts.
this.parts = document.createElement('a');
this.parts.href = this.url;
}
return this.parts;
},
params: function () {
var buffer;
if (!this.hasOwnProperty('params')) {
// Extract and attach the query parameters.
this.params = {};
buffer = {search: this.getParts().search};
buffer.search = buffer.search.substring(1);
if (buffer.search.length === 0) {
// No params given, abort extracting.
return this.params;
}
buffer.queries = buffer.search.split('&');
buffer.ql = buffer.queries.length;
for (buffer.i = 0; buffer.i < buffer.ql; buffer.i++) {
buffer.current = buffer.queries[buffer.i].split('=');
if (buffer.current[0].length === 0) {
continue;
}
if (buffer.current.length === 2) {
this.params[buffer.current[0]] = buffer.current[1];
}
else {
this.params[buffer.current[0]] = '';
}
}
}
return this.params;
}
}
};
sync.now = sync.now || function () {
var registry = sync.registry;
var init = sync.___internals.item.init;
var start = sync.___internals.item.start;
var item;
var template;
var i;
i = sync.templates.length;
while (i > 0) {
i--;
template = sync.templates.shift();
registry.templates[template.name] = template.content;
}
i = registry.pending.length;
while (i > 0) {
i--;
item = registry.pending.shift();
if (item.ready()) {
start.call(item);
}
else {
registry.pending.push(item);
}
}
i = sync.items.length;
while (i > 0) {
i--;
item = sync.items.shift();
init.call(item);
registry.items.push(item);
if (item.ready()) {
start.call(item);
}
else {
registry.pending.push(item);
}
}
};
sync.refresh = sync.refresh || function (dom) {
var listeners = sync.registry.listeners;
var selector;
var group;
var new_elements;
var event_name;
var event_item;
var i;
var targetElement;
var length;
var k;
var length_k;
sync.now();
if (typeof dom === 'undefined') {
dom = document;
}
else if (dom === null) {
return;
}
// Collect all triggering elements,
// and register the corresponding event listeners.
for (selector in listeners) {
if (!listeners.hasOwnProperty(selector)) {
continue;
}
group = listeners[selector];
new_elements = dom.querySelectorAll(selector);
length = new_elements.length;
for (i = 0; i < length; i++) {
targetElement = new_elements[i];
if (group.elements.indexOf(targetElement) < 0) {
group.elements.push(targetElement);
}
for (event_name in group.events) {
if (!group.events.hasOwnProperty(event_name)) {
continue;
}
event_item = group.events[event_name];
length_k = group.elements.length;
for (k = 0; k < length_k; k++) {
group.elements[k].addEventListener(event_name, event_item.triggered, false);
}
}
}
}
};
}(Drupal, mustacheSync, Mustache));
