/
template.js
185 lines (179 loc) · 5.58 KB
/
template.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
class HTMLElementTemplate extends HTMLCustomElement {
static get defaults() {
return {
action: null
};
}
static prepareTemplate(node) {
var doc = node.ownerDocument;
var tmpl = node;
var helper;
if (node.matches('script[type="text/html"]')) {
helper = doc.createElement('div');
helper.innerHTML = node.textContent;
tmpl = doc.createElement('template');
if (!tmpl.content) tmpl.content = doc.createDocumentFragment();
tmpl.content.appendChild(node.dom(helper.textContent));
node.replaceWith(tmpl);
node.textContent = helper.textContent = '';
}
if (tmpl.isContentEditable) {
if (tmpl.content && tmpl.children.length == 0) {
tmpl.textContent = '';
tmpl.appendChild(tmpl.content);
}
} else if (document.visibilityState == "prerender") {
var dest = tmpl.dom(`<script type="text/html"></script>`);
if (!helper) helper = doc.createElement('div');
helper.textContent = tmpl.content.childNodes.map(child => {
if (child.nodeType == Node.TEXT_NODE) return child.nodeValue;
else return child.outerHTML;
}).join('');
dest.textContent = helper.innerHTML;
dest.content = tmpl.content;
tmpl.replaceWith(dest);
tmpl = dest;
}
return tmpl;
}
patch(state) {
HTMLElementTemplate.prepareTemplate(this.firstElementChild);
if (this.isContentEditable || this._refreshing || this.closest('[block-content="template"]')) return;
// first find out if state.query has a key in this.keys
// what do we do if state.query has keys that are used by a form in this query template ?
var expr = this.getAttribute('block-expr');
var vars = {};
var opts = this.options;
var scope = state.scope;
if (expr) {
try {
expr = JSON.parse(expr);
} catch(ex) {
console.warn("block-expr attribute should contain JSON");
expr = {};
}
var missing = 0;
var filters = scope.$filters;
scope.$filters = Object.assign({}, filters, {
'|': function(val, what) {
var path = what.scope.path.slice();
if (path[0] == "$query") {
var name = path.slice(1).join('.');
if (name.length && state.query[name] !== undefined) {
val = state.query[name];
}
}
return val;
},
'||': function(val, what) {
var path = what.scope.path.slice();
if (path[0] == "$query") {
var name = path.slice(1).join('.');
if (name.length) {
if (val !== undefined) {
if (val != null) vars[name] = val;
} else {
missing++;
}
}
}
}
});
Pageboard.merge(expr, function(val) {
if (typeof val == "string") try {
return val.fuse({$query: state.query}, scope);
} catch(ex) {
return val;
}
});
scope.$filters = filters;
Object.keys(vars).forEach(function(key) {
state.vars[key] = true;
});
if (missing) return;
} else if (!opts.action) {
// non-remotes cannot know if they will need $query
}
var loader;
if (opts.action) {
var queryStr = Page.format({pathname: "", query: vars});
if (queryStr == this.dataset.query) return;
this.dataset.query = queryStr;
loader = Pageboard.fetch('get', opts.action, vars);
} else {
loader = Promise.resolve();
}
this._refreshing = true;
this.classList.remove('error', 'warning', 'success');
if (opts.action) this.classList.add('loading');
return Pageboard.bundle(loader, state).then((res) => {
this.render(res, state);
}).catch(function(err) {
state.scope.$status = -1;
console.error("Error building", err);
}).then(() => {
var name = '[$status|statusClass]'.fuse(state.scope);
if (name) this.classList.add(name);
this.classList.remove('loading');
this._refreshing = false;
});
}
render(data, state) {
if (this.children.length != 2) return;
var tmpl = this.firstElementChild.content.cloneNode(true);
var view = this.lastElementChild;
// remove all block-id from template - might be done in pagecut eventually
var rnode;
while ((rnode = tmpl.querySelector('[block-id]'))) rnode.removeAttribute('block-id');
// pagecut merges block-expr into block-data - contrast with above patch() method
while ((rnode = tmpl.querySelector('[block-expr]'))) rnode.removeAttribute('block-expr');
var scope = Object.assign({}, state.scope);
var usesQuery = false;
var el = {
name: 'element_template_' + (Math.round(Date.now() * Math.random()) + '').substr(-6),
dom: tmpl,
filters: {
'||': function(val, what) {
var path = what.scope.path;
if (path[0] != "$query") return;
usesQuery = true;
var key;
if (path.length > 1) {
// (b)magnet sets val to null so optional values are not undefined
key = path.slice(1).join('.');
var undef = val === undefined;
if (!state.vars[key]) {
if (undef) console.info("$query." + key, "is undefined");
state.vars[key] = !undef;
}
} else {
for (key in state.query) state.vars[key] = true;
}
}
},
// contents: tmpl.querySelectorAll('[block-content]').map((node) => {
// return {
// id: node.getAttribute('block-content'),
// nodes: 'block+'
// };
// })
};
Object.keys(state.data).forEach(function(key) {
if (key.startsWith('$') && scope[key] == null) scope[key] = state.data[key];
});
scope.$pathname = state.pathname;
scope.$query = state.query;
scope.$referrer = state.referrer.pathname || state.pathname;
var node = Pageboard.render(data, scope, el);
view.textContent = '';
view.appendChild(node);
if (usesQuery) state.scroll({
once: true,
node: this.parentNode,
behavior: 'smooth'
});
}
}
Page.ready(function() {
HTMLCustomElement.define('element-template', HTMLElementTemplate);
});