-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbrick-element.js
272 lines (270 loc) · 12.3 KB
/
brick-element.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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/****************************************************************************************
* litRead: Template literal parser *
* **************************************************************************************
* Concatenates placeholder ${ } that contains string or arrays of strings with the text
* Extract <template> from placeholder ${ } (mainly for <styles> import).
* Insert automatic ID in text if placeholder contain a string starting with '#'
* Extract config information in placeholders that contains '|* *|' signature .
*
* Returns an object of the type:
* {template: "...", props:{"...",...}, IDs:["..",...], imports:[<template>,...]}
*/
function inputError(input) {
console.log('LitRead does not accept the following ${ } as input in string literal:');
console.log(input);
throw Error('Invalid input.');
}
export function litRead(strings, ...keys) {
let output;
output = { template: "", props: {}, imports: [], IDs: [] };
if (strings.length <= keys.length)
throw Error('Improper parameter size.');
if (strings.length === 1) {
output.template = `${strings[0]}`;
return output;
}
// from here there is at least one key
let temp_str = "";
strings.forEach((str_val, index) => {
temp_str += str_val;
if (index === keys.length)
return;
let key = keys[index];
// cases:
if (typeof (key) === 'string') {
let trimmed = key;
trimmed.trim();
// case of and ID
if (trimmed[0] === "#" && trimmed[1] === "-") {
temp_str += ` id="${trimmed.substring(2)}" `;
output.IDs.push(trimmed.substring(2));
}
// case of an attribute
else if (trimmed.slice(0, 2) === '|*' && trimmed.slice(-2) === '*|') {
let no_space = trimmed.replace(/\s/g, '');
let properties = no_space.slice(2, -2).split('|');
for (let p of properties) {
if (p.includes('-b'))
output.props[p.replace(/\-b/g, '')] = 'bool';
else
output.props[p] = 'string';
}
}
// case of expression or string
else
temp_str += key;
}
else if (typeof (key) === 'object') {
// case of a list of strings or templates
if (Array.isArray(key)) {
for (let val of key) {
if (typeof (val) === 'string')
temp_str += ' ' + val;
else if (typeof (val) === 'object' && 'tagName' in val && val.tagName === 'TEMPLATE') {
output.imports.push(val);
}
else
inputError(val);
}
}
// case of a template
else if ('tagName' in key && key.tagName === 'TEMPLATE') {
output.imports.push(key);
// in case template is from templateme import IDs and props
if (key.hasOwnProperty("_props") && key._props != null && key._props != undefined)
for (let p_name in key._props) {
this._props[p_name] = key._props[p_name];
}
if (key.hasOwnProperty("_IDs") && key._IDs != null && key._IDs != undefined)
output.IDs.concat(key._IDs);
}
else
inputError(key);
}
else if (typeof (key) === 'number') {
temp_str += key.toString(10);
}
else
inputError(key);
});
output.template = temp_str;
return output;
}
/****************************************************************************************
* templateme: Templates generator *
* **************************************************************************************
* Using litRead creates an instance of template in the document.
* To be used as a tag for a string literal.
* Returns a <template>.
*/
export function templateme(strings, ...keys) {
// NOTE on performance: it is a bit faster this way using insertBefore instead of appendChild,
// because in that case there is an additional document.createElement for the additional appended child.
let read_inputs = litRead(strings, ...keys);
let out_template = document.createElement('template');
out_template.innerHTML = read_inputs.template;
/*
// THIS DOES NOT WORK
for (let tmpl of read_inputs.imports) {
//out_template.insertBefore(tmpl.content.cloneNode(true), out_template.childNodes[0] || null);
out_template.appendChild(tmpl.content.cloneNode(true)); // FIXME: cannot add child totemplate
}
*/
Object.defineProperty(out_template, '_props', read_inputs.props);
Object.defineProperty(out_template, '_IDs', read_inputs.IDs);
return out_template;
}
/**
* Custom-element mixing generator, to be used as a tagged literal. It returns a mixin function that takes two arguments.
* @param class - the base class to which apply the mixin, can be HTMLElement or can inherit from a custom element
* @param configs - object with structure {inherit:boolean, shadowRoot: { mode:string, delegatesFocus:boolean } }
* default values of configs are inherit:false, mode:open, delegatesFocus:false.
*/
export function brick(strings, ...keys) {
let litOut = litRead(strings, ...keys);
let tmpl = document.createElement('template');
tmpl.innerHTML = litOut.template;
litOut.imports.push(tmpl);
// Typescript: FIXME, would be nice to return at least an HTMLElement,
// but cannot make it work
return function (BaseClass, config) {
return class extends BaseClass {
constructor(...args) {
super();
// copy props, this works also in case of inheritance
if (!this._props)
this._props = {};
for (let key in litOut.props) {
this._props[key] = litOut.props[key];
}
// attach shadow or inherit shadow
let conf = (config && config.shadowRoot) ? config.shadowRoot : { mode: 'open', delegatesFocus: false };
let shadowRoot = (config && config.inherit) ? this.shadowRoot : this.attachShadow(conf);
for (let tmpl of litOut.imports) {
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
// attach elements IDs
if (!this.ids)
this.ids = {};
for (let id of litOut.IDs) {
this.ids[id] = shadowRoot.getElementById(id);
}
this.qs = this.shadowRoot.querySelector;
this.swr = this.shadowRoot;
// set the attribute-property reflection, does not re-define props in case of inheritance
this.setProps();
// define getters and setters for brick-slots, in case of inheritance does not re-define
this.acquireSlots();
this.setRootToChilds();
}
static get observedAttributes() {
let arr = [];
if (super.observedAttributes) {
arr = super.observedAttributes;
}
return arr.concat(Object.keys(litOut.props));
}
setProps() {
// define getters and setters for list of properties
// in case of inheritance does not re-define props
for (let prop in this._props) {
if (!this.hasOwnProperty(prop)) {
Object.defineProperty(this, prop, {
set: (val) => { this.setAttribute(prop, val); },
get: () => { return this.getAttribute(prop); }
});
}
}
}
acquireSlots() {
let slots = this.swr.querySelectorAll("slot");
for (let s of slots) {
if (s.hasAttribute("type")) {
let name = s.getAttribute("name");
let type = s.getAttribute("type");
// NOTE: does not re-define in case of inheritace
if (name === "" || type === "" || name === null || this.hasOwnProperty(name))
continue;
Object.defineProperty(this, name, {
set: (data) => {
let temp = this.querySelectorAll(`[slot=${name}]`);
// empty all
for (let t of temp) {
this.removeChild(t);
}
// re-create
let array_data = [];
if (Array.isArray(data))
array_data = data;
else
array_data.push(data);
for (let obj of array_data) {
let el = document.createElement(type);
el.setAttribute("slot", name);
for (let key in obj) {
// @ts-ignore FIXME
if (typeof (el[key]) !== "undefined") {
// @ts-ignore FIXME
el[key] = obj[key];
}
else {
console.log("EROR: key '", key, "' not assignable to class ", el.tagName);
}
}
this.appendChild(el);
}
},
get: () => {
let temp = this.querySelectorAll(`[slot=${name}]`);
if (temp.length === 1)
return temp[0];
else
return temp;
}
});
}
}
}
ingestData(input) {
for (let key in input) {
if (typeof (this[key]) !== "undefined") {
//@ts-ignore // FIXME here not the best... would be nice to have typescript recognice keys and their types
this[key] = input[key];
}
else {
console.log("EROR: key '", key, "' not assignable to class ", this.className);
}
}
}
setRootToChilds() {
let all = this.swr.querySelectorAll("*");
for (let el of all) {
// @ts-ignore
el["root"] = this;
}
}
/*
/// SUPPORT FOR DEFAULT values on attributes REVOKED. Attributes are behaviours, defaults make no sense.
connectedCallback() {
if(super['connectedCallback'] !== undefined ) super.connectedCallback();
for (let prop in this._props) {
if (!this.hasAttribute(prop) && Array.isArray(this._props[prop]) ) this.setAttribute(prop, this._props[prop][1]);
}
}
*/
attributeChangedCallback(name, oldVal, newVal) {
const hasValue = (newVal !== null);
const updateMe = (!hasValue || oldVal !== newVal);
if (updateMe && this._props.hasOwnProperty(name) && this['update_' + name] !== undefined) {
this['update_' + name](newVal);
}
}
};
};
}
// some shortcuts:
export let dfn = customElements.define.bind(customElements);
export function ifNdef(name, cls) {
if (!customElements.get(name))
customElements.define(name, cls);
}