@master/css-runtime 2.0.0-rc.48 → 2.0.0-rc.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -32,10 +32,7 @@ class RuntimeCSS extends css.MasterCSS {
32
32
  * Observe the DOM for changes and update the running stylesheet. (browser only)
33
33
  * @param options mutation observer options
34
34
  * @returns this
35
- */ observe(options = {
36
- subtree: true,
37
- childList: true
38
- }) {
35
+ */ observe() {
39
36
  if (this.observing) return this;
40
37
  if (this.root.styleSheets) for (const sheet of this.root.styleSheets){
41
38
  const { ownerNode } = sheet;
@@ -55,164 +52,159 @@ class RuntimeCSS extends css.MasterCSS {
55
52
  this.style.sheet.insertRule(this.layerStatementRule.text);
56
53
  this.layerStatementRule.nodes[0].native = this.style.sheet.cssRules.item(0);
57
54
  }
58
- const handleClassList = (classList)=>{
59
- classList.forEach((className)=>{
60
- if (Object.prototype.hasOwnProperty.call(this.classesUsage, className)) {
61
- this.classesUsage[className]++;
55
+ const firstConnectedElementsClasses = new Map();
56
+ const addClasses = (element)=>{
57
+ element.classList.forEach((className)=>{
58
+ if (this.classUsages.has(className)) {
59
+ this.classUsages.set(className, this.classUsages.get(className) + 1);
62
60
  } else {
63
- this.classesUsage[className] = 1;
61
+ this.classUsages.set(className, 1);
64
62
  this.add(className);
65
63
  }
64
+ const classes = firstConnectedElementsClasses.get(element);
65
+ if (classes) {
66
+ classes.push(className);
67
+ } else {
68
+ firstConnectedElementsClasses.set(element, [
69
+ className
70
+ ]);
71
+ }
66
72
  });
67
73
  };
68
- handleClassList(this.host.classList);
69
- if (options.subtree) {
70
- /**
71
- * 待所有 DOM 結構完成解析後,開始繪製 Rule 樣式
72
- */ (this.root.constructor.name === 'HTMLDocument' ? this.host : this.container).querySelectorAll('[class]').forEach((element)=>handleClassList(element.classList));
73
- }
74
+ addClasses(this.host);
75
+ /**
76
+ * 待所有 DOM 結構完成解析後,開始繪製 Rule 樣式
77
+ */ (this.root.constructor.name === 'HTMLDocument' ? this.host : this.container).querySelectorAll('[class]').forEach((element)=>{
78
+ addClasses(element);
79
+ });
80
+ // @ts-expect-error readonly
74
81
  this.observer = new MutationObserver((mutationRecords)=>{
75
- // console.time('css engine');
76
- const correctionOfClassName = {};
77
- const attributeMutationRecords = [];
78
- const updatedElements = [];
79
- const unchangedElements = [];
80
- /**
81
- * 取得所有深層後代的 class names
82
- */ const handleClassNameDeeply = (element, remove)=>{
83
- if (remove) {
84
- element.classList.forEach(removeClassName);
85
- } else {
86
- element.classList.forEach(addClassName);
87
- }
88
- const children = element.children;
89
- for (const eachChildren of children){
90
- if (eachChildren.classList && !updatedElements.includes(eachChildren)) {
91
- updatedElements.push(eachChildren);
92
- handleClassNameDeeply(eachChildren, remove);
93
- }
94
- }
82
+ // console.clear()
83
+ // console.log('-----------')
84
+ // const test = 'swiper-slide-next'
85
+ // console.log(`${test}: ${this.classUsages.get(test)}`)
86
+ const eachClassUsages = new Map();
87
+ const updatedAttrElements = new Set();
88
+ const updatedConnectedElements = new Set();
89
+ const updatedElements = new Set();
90
+ const updateClassUsage = (classes, isAdding = false)=>{
91
+ const usage = isAdding ? 1 : -1;
92
+ classes.forEach((className)=>{
93
+ // if (className === test) console.log(` ${isAdding ? '+' : '-'} ${className} ${(eachClassUsages.get(className) || 0) + usage}`, target)
94
+ eachClassUsages.set(className, (eachClassUsages.get(className) || 0) + usage);
95
+ });
95
96
  };
96
- const addClassName = (className)=>{
97
- if (Object.prototype.hasOwnProperty.call(correctionOfClassName, className)) {
98
- correctionOfClassName[className]++;
99
- } else {
100
- correctionOfClassName[className] = 1;
97
+ const updateElementTree = (element, adding)=>{
98
+ if (element.isConnected) {
99
+ updatedConnectedElements.add(element);
101
100
  }
102
- };
103
- const removeClassName = (className)=>{
104
- if (Object.prototype.hasOwnProperty.call(correctionOfClassName, className)) {
105
- correctionOfClassName[className]--;
106
- } else if (Object.prototype.hasOwnProperty.call(this.classesUsage, className)) {
107
- correctionOfClassName[className] = -1;
101
+ updatedElements.add(element);
102
+ updateClassUsage(element.classList, adding);
103
+ for (const child of element.children){
104
+ updateElementTree(child, adding);
108
105
  }
109
106
  };
110
- const handleNodes = (nodes, remove)=>{
111
- for (const eachNode of nodes){
112
- if (eachNode.classList && !updatedElements.includes(eachNode) && !unchangedElements.includes(eachNode)) {
113
- if (eachNode.isConnected !== remove) {
114
- updatedElements.push(eachNode);
115
- handleClassNameDeeply(eachNode, remove);
116
- } else {
117
- unchangedElements.push(eachNode);
118
- }
119
- }
107
+ // console.log('///')
108
+ mutationRecords.forEach((mutation)=>{
109
+ const target = mutation.target;
110
+ switch(mutation.type){
111
+ case 'attributes':
112
+ const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : [];
113
+ const newClassList = target.classList;
114
+ newClassList.forEach((c)=>{
115
+ if (!oldClassList.includes(c)) ;
116
+ });
117
+ break;
120
118
  }
121
- };
122
- for (const mutationRecord of mutationRecords){
123
- const { addedNodes, removedNodes, type, target } = mutationRecord;
124
- if (type === 'attributes') {
125
- /**
126
- * 防止同樣的 MutationRecord 重複執行
127
- * According to this history,
128
- * MutationObserver was designed to work that way.
129
- * Any call to setAttribute triggers a mutation,
130
- * regardless of whether the value is being changed or set to the current value
131
- */ if (attributeMutationRecords.find((eachAttributeMutationRecord)=>eachAttributeMutationRecord.target === target)) {
132
- continue;
133
- } else {
119
+ });
120
+ // console.log('///')
121
+ mutationRecords.forEach((mutation)=>{
122
+ const target = mutation.target;
123
+ switch(mutation.type){
124
+ case 'attributes':
134
125
  /**
135
- * 第一個匹配到的 oldValue 一定是該批變動前的原始狀態值
136
- */ attributeMutationRecords.push(mutationRecord);
137
- }
138
- } else {
139
- // 先判斷節點新增或移除
140
- handleNodes(addedNodes, false);
141
- // 忽略處理新元素的已刪除子節點
142
- if (!target.isConnected || !updatedElements.includes(target)) {
143
- handleNodes(removedNodes, true);
144
- }
126
+ * We only need to determine the first attribute record of the same target,
127
+ * because the first record has the original old class name.
128
+ *
129
+ * The target is skipped if it is contained in the unhandled childList.
130
+ */ if (updatedAttrElements.has(target) || updatedElements.has(target)) return;
131
+ updatedAttrElements.add(target);
132
+ const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : [];
133
+ const newClassList = target.classList;
134
+ const addedClasses = [];
135
+ newClassList.forEach((c)=>{
136
+ if (!oldClassList.includes(c)) addedClasses.push(c);
137
+ });
138
+ const removedClasses = oldClassList.filter((c)=>!newClassList.contains(c));
139
+ // if (addedClasses.length) console.log('[attribute]', '[add]', addedClasses, target)
140
+ if (addedClasses.length) updateClassUsage(addedClasses, true);
141
+ // if (removedClasses.length) console.log('[attribute]', '[remove]', removedClasses, target)
142
+ if (removedClasses.length) updateClassUsage(removedClasses, false);
143
+ break;
144
+ case 'childList':
145
+ if (updatedConnectedElements.has(target)) return;
146
+ // if (mutation.addedNodes.length) console.log('[childList]', '[add]', mutation.addedNodes)
147
+ mutation.addedNodes.forEach((node)=>'classList' in node && updateElementTree(node, true));
148
+ // if (mutation.removedNodes.length) console.log('[childList]', '[remove]', mutation.removedNodes)
149
+ mutation.removedNodes.forEach((node)=>'classList' in node && updateElementTree(node, false));
150
+ break;
145
151
  }
146
- }
147
- if (!attributeMutationRecords.length && !Object.keys(correctionOfClassName).length) {
148
- // console.timeEnd('css engine');
149
- return;
150
- }
151
- for (const { oldValue, target } of attributeMutationRecords){
152
- /**
153
- * 如果被操作的元素中包含了屬性變更的目標,
154
- * 則將該目標從 existedAttributeMutationTargets 中移除,
155
- * 以防止執行接下來的屬性變更處理
156
- *
157
- * 該批 mutationRecords 中,某個 target 同時有 attribute 及 childList 的變更,
158
- * 則以 childList 節點插入及移除的 target.className 為主
159
- */ const updated = updatedElements.includes(target);
160
- const classNames = target.classList;
161
- const oldClassNames = oldValue ? oldValue.split(' ') : [];
162
- if (updated) {
163
- if (target.isConnected) {
164
- continue;
165
- } else {
166
- for (const oldClassName of oldClassNames){
167
- if (!classNames.contains(oldClassName)) {
168
- removeClassName(oldClassName);
169
- }
170
- }
171
- }
172
- } else if (target.isConnected) {
173
- classNames.forEach((className)=>{
174
- if (!oldClassNames.includes(className)) {
175
- addClassName(className);
176
- }
177
- });
178
- for (const oldClassName of oldClassNames){
179
- if (!classNames.contains(oldClassName)) {
180
- removeClassName(oldClassName);
181
- }
152
+ });
153
+ /**
154
+ * Merge the class usage changes into the current class usage map.
155
+ */ eachClassUsages.forEach((countChange, className)=>{
156
+ let currentCount = this.classUsages.get(className) || 0;
157
+ let newCount = currentCount + countChange;
158
+ if (newCount > 0) {
159
+ this.classUsages.set(className, newCount);
160
+ if (currentCount === 0) {
161
+ this.add(className);
182
162
  }
183
- }
184
- }
185
- for(const className in correctionOfClassName){
186
- const correction = correctionOfClassName[className];
187
- const count = (this.classesUsage[className] || 0) + correction;
188
- if (count === 0) {
189
- // remove
190
- delete this.classesUsage[className];
191
- /**
192
- * class name 從 DOM tree 中被移除,
193
- * 匹配並刪除對應的 rule
194
- */ this.remove(className);
195
163
  } else {
196
- if (!Object.prototype.hasOwnProperty.call(this.classesUsage, className)) {
197
- // add
198
- /**
199
- * 新 class name 被 connected 至 DOM tree,
200
- * 匹配並創建對應的 Rule
201
- */ this.add(className);
202
- }
203
- this.classesUsage[className] = count;
164
+ this.classUsages.delete(className);
165
+ this.remove(className);
204
166
  }
205
- }
167
+ });
168
+ // start: debug
169
+ // const safeClassUsages: any = {};
170
+ // ((this.root.constructor.name === 'HTMLDocument') ? this.host : this.container)
171
+ // .querySelectorAll('[class]')
172
+ // .forEach((element) => {
173
+ // element.classList.forEach((className) => {
174
+ // if (Object.prototype.hasOwnProperty.call(safeClassUsages, className)) {
175
+ // safeClassUsages[className]++
176
+ // } else {
177
+ // safeClassUsages[className] = 1
178
+ // }
179
+ // })
180
+ // })
181
+ // this.host.classList.forEach((className) => {
182
+ // if (Object.prototype.hasOwnProperty.call(safeClassUsages, className)) {
183
+ // safeClassUsages[className]++
184
+ // } else {
185
+ // safeClassUsages[className] = 1
186
+ // }
187
+ // })
188
+ // // 與 this.classUsages 比較並且打印不同的部分
189
+ // for (const className in safeClassUsages) {
190
+ // if (this.classUsages.get(className) !== safeClassUsages[className]) {
191
+ // throw new Error(`[css] ${className} ${this.classUsages.get(className)} (correct: ${safeClassUsages[className]})`)
192
+ // }
193
+ // }
194
+ // end: debug
206
195
  });
207
196
  this.observer.observe(this.root, {
208
- ...options,
209
197
  attributes: true,
210
198
  attributeOldValue: true,
211
199
  attributeFilter: [
212
200
  'class'
213
- ]
201
+ ],
202
+ childList: true,
203
+ subtree: true
214
204
  });
215
- this.host.style.removeProperty('display');
205
+ if (!this.progressive) {
206
+ this.host.style.removeProperty('display');
207
+ }
216
208
  // @ts-ignore
217
209
  this.observing = true;
218
210
  return this;
@@ -307,7 +299,7 @@ class RuntimeCSS extends css.MasterCSS {
307
299
  handleSyntaxLayer(this.presetLayer);
308
300
  break;
309
301
  case 'components':
310
- this.stylesLayer.native = cssLayerBlockRule;
302
+ this.componentsLayer.native = cssLayerBlockRule;
311
303
  let stylePreText;
312
304
  const createSyntaxRules = (cssRule)=>{
313
305
  if (cssRule.selectorText) {
@@ -332,10 +324,10 @@ class RuntimeCSS extends css.MasterCSS {
332
324
  for (const node of eachSyntaxRule.nodes){
333
325
  if (!node.native && node.text.includes(stylePreText)) {
334
326
  node.native = cssRule;
335
- if (!this.stylesLayer.rules.includes(eachSyntaxRule)) {
336
- this.stylesLayer.rules.push(eachSyntaxRule);
337
- this.stylesLayer.insertVariables(eachSyntaxRule);
338
- this.stylesLayer.insertAnimations(eachSyntaxRule);
327
+ if (!this.componentsLayer.rules.includes(eachSyntaxRule)) {
328
+ this.componentsLayer.rules.push(eachSyntaxRule);
329
+ this.componentsLayer.insertVariables(eachSyntaxRule);
330
+ this.componentsLayer.insertAnimations(eachSyntaxRule);
339
331
  }
340
332
  matched = true;
341
333
  break;
@@ -348,14 +340,14 @@ class RuntimeCSS extends css.MasterCSS {
348
340
  console.error(`Cannot recognize the CSS rule in the components layer. \`${cssRule.cssText}\` (https://rc.css.master.co/messages/hydration-errors)`);
349
341
  }
350
342
  }
351
- for (const eachRule of this.stylesLayer.rules){
343
+ for (const eachRule of this.componentsLayer.rules){
352
344
  for(let k = eachRule.nodes.length - 1; k >= 0; k--){
353
345
  if (!eachRule.nodes[k].native) {
354
346
  eachRule.nodes.splice(k, 1);
355
347
  }
356
348
  }
357
349
  }
358
- if (this.stylesLayer.rules.length) this.rules.push(this.stylesLayer);
350
+ if (this.componentsLayer.rules.length) this.rules.push(this.componentsLayer);
359
351
  break;
360
352
  case 'general':
361
353
  handleSyntaxLayer(this.generalLayer);
@@ -367,13 +359,14 @@ class RuntimeCSS extends css.MasterCSS {
367
359
  if (!this.observing) return;
368
360
  if (this.observer) {
369
361
  this.observer.disconnect();
370
- this.observer = null;
362
+ // @ts-expect-error readonly
363
+ this.observer = undefined;
371
364
  }
372
365
  // @ts-ignore
373
366
  this.observing = false;
374
367
  this.reset();
375
368
  // @ts-expect-error
376
- this.classesUsage = {};
369
+ this.classUsages = {};
377
370
  if (!this.progressive) {
378
371
  this.style?.remove();
379
372
  this.style = null;