Skip to content

Commit

Permalink
[gem] Support DevTools modify attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Jun 17, 2024
1 parent fa941f1 commit 2c8f7d5
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 55 deletions.
1 change: 1 addition & 0 deletions packages/gem-devtools/src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './manifest.json';
type DevToolsHookStore = {
customElementMap: Map<string, CustomElementConstructor>;
currentElementsMap: Map<string, Element>;
domAttrMutation?: MutationObserver;
};
declare global {
interface Window {
Expand Down
7 changes: 7 additions & 0 deletions packages/gem-devtools/src/devtools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { devtools } from 'webextension-polyfill';

import { execution } from './common';
import { observeSelectedElement } from './scripts/observer';

devtools.panels.elements.createSidebarPane('Gem').then((sidebar) => {
// sidebar.setPage('test.html');
sidebar.setPage('sidebarpanel.html');
});

devtools.panels.elements.onSelectionChanged.addListener(() => {
execution(observeSelectedElement, []);
});
34 changes: 22 additions & 12 deletions packages/gem-devtools/src/scripts/get-gem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,24 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str

const kebabToCamelCase = (str: string) => str.replace(/-(.)/g, (_substr, $1: string) => $1.toUpperCase());
const attrs: Set<string> = $0.attributes ? new Set([...$0.attributes].map((attr) => attr.nodeName)) : new Set();
const elementMethod = new Set([
'connectedCallback',
'attributeChangedCallback',
'adoptedCallback',
'disconnectedCallback',
'setAttribute',
'removeAttribute',
'toggleAttribute',
]);
const lifecycleMethod = new Set(['willMount', 'render', 'mounted', 'shouldUpdate', 'updated', 'unmounted']);
const buildInLifecycleMethod = new Set(['connectedCallback', 'attributeChangedCallback', 'disconnectedCallback']);
const buildInMethod = new Set(['update', 'setState', 'effect', 'memo', 'closestElement']);
const buildInProperty = new Set(['internals']);
const buildInAttribute = new Set(['ref']);
const member = getProps($0);
const memberSet = getProps($0);
tagClass.observedAttributes?.forEach((attr) => {
const prop = kebabToCamelCase(attr);
const value = $0[prop];
member.delete(prop);
memberSet.delete(prop);
attrs.delete(attr);
data.observedAttributes.push({
name: attr,
Expand All @@ -119,7 +127,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
});
});
tagClass.observedProperties?.forEach((prop) => {
member.delete(prop);
memberSet.delete(prop);
const value = $0[prop];
const type = value === null ? 'null' : typeof value;
data.observedProperties.push({
Expand All @@ -131,7 +139,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
});
tagClass.definedEvents?.forEach((event) => {
const prop = kebabToCamelCase(event);
member.delete(prop);
memberSet.delete(prop);
data.emitters.push({
name: event,
value: objectToString($0[prop]),
Expand Down Expand Up @@ -159,7 +167,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
const isUnnamed = slot === 'unnamed';
const prop = kebabToCamelCase(slot);
if (!$0.constructor[prop]) {
member.delete(prop);
memberSet.delete(prop);
}
const selector = `[slot=${slot}]`;
let element = isUnnamed ? $0.firstChild : $0.querySelector(selector);
Expand All @@ -178,7 +186,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
tagClass.definedParts?.forEach((part) => {
const prop = kebabToCamelCase(part);
if (!$0.constructor[prop]) {
member.delete(prop);
memberSet.delete(prop);
}
const selector = `[part~=${part}],[exportparts*=${part}]`;
data.parts.push({
Expand All @@ -190,7 +198,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
});
tagClass.definedRefs?.forEach((ref) => {
const prop = kebabToCamelCase(ref.replace(/-\w+$/, ''));
member.delete(prop);
memberSet.delete(prop);
data.refs.push({
name: ref,
value: objectToString($0[prop].element),
Expand All @@ -200,17 +208,17 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
});
tagClass.definedCSSStates?.forEach((state) => {
const prop = kebabToCamelCase(state);
member.delete(prop);
memberSet.delete(prop);
data.cssStates.push({
name: state,
value: $0[prop],
type: 'boolean',
});
});
member.forEach((key) => {
member.delete(key);
memberSet.forEach((key) => {
memberSet.delete(key);
// GemElement 不允许覆盖内置生命周期,所以不考虑
if (buildInLifecycleMethod.has(key)) return;
if (elementMethod.has(key)) return;
if (key === 'state') {
$0.state &&
Object.keys($0.state).forEach((k) => {
Expand Down Expand Up @@ -265,6 +273,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
'definedParts',
'definedSlots',
]);
const buildInShowStaticMember = new Set(['rootElement']);
const getStaticMember = (cls: any, set = new Set<string>()) => {
Object.getOwnPropertyNames(cls).forEach((key) => {
if (
Expand All @@ -287,6 +296,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str
type: typeof value,
value: objectToString(value),
path: inspectable(value) ? ['constructor', key] : undefined,
buildIn: buildInShowStaticMember.has(key) ? 1 : 0,
});
});
// `Class` self
Expand Down
22 changes: 22 additions & 0 deletions packages/gem-devtools/src/scripts/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
declare let $0: any;

export function observeSelectedElement() {
const { __GEM_DEVTOOLS__STORE__ } = window;
if (!__GEM_DEVTOOLS__STORE__ || !$0) return;

const observer = (__GEM_DEVTOOLS__STORE__.domAttrMutation ||= new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
if (mutation.type === 'attributes') {
const ele = mutation.target as Element;
const attr = ele.getAttribute(mutation.attributeName!);
if (attr === null) {
ele.removeAttribute(mutation.attributeName!);
} else if (attr !== mutation.oldValue) {
ele.setAttribute(mutation.attributeName!, attr);
}
}
}
}));
observer.disconnect();
observer.observe($0, { attributeOldValue: true });
}
6 changes: 3 additions & 3 deletions packages/gem/docs/en/001-guide/002-advance/010-debug.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Debug

GemElement is a standard custom element that can be inspected and debugged in the browser DevTools. You can also install the [Gem Devtools](https://chrome.google.com/webstore/detail/gem-devtools/lgfpciakeemopebkmjajengljoakjfle) extension, which allows you to more intuitively inspect and modify the data of Gem elements.
GemElement is a standard custom element that can be inspected and debugged in the browser DevTools. You can also install the [Gem DevTools](https://chrome.google.com/webstore/detail/gem-devtools/lgfpciakeemopebkmjajengljoakjfle) extension, which allows you to more intuitively inspect and modify the data of Gem elements.

## Use Gem Devtools
## Use Gem DevTools

![Gem Devtools](https://raw.githubusercontent.com/mantou132/gem/master/packages/gem-devtools/screenshot/firefox.jpg)
![Gem DevTools](https://raw.githubusercontent.com/mantou132/gem/master/packages/gem-devtools/screenshot/firefox.jpg)
5 changes: 4 additions & 1 deletion packages/gem/docs/en/004-blog/006-es-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ let MyElement = (() => {

## Pitfalls of using ES Decorators

`@attribute` no longer work through the`observedAttributes`, but intercept the `setAttribute`. Do not use the modified` setAttribute` in Devtools, so modify the element attribute in Devtools cannot trigger the element update.
`@attribute` no longer work through the`observedAttributes`, but intercept the `setAttribute`. Do not use the modified` setAttribute` in DevTools, so modify the element attribute in DevTools cannot trigger the element update.

> [!NOTE]
> Installing the browser extension [Gem DevTools](https://chrome.google.com/webstore/detail/gem-devtools/lgfpciakeemopebkmjajengljoakjfle) will solve this problem
6 changes: 3 additions & 3 deletions packages/gem/docs/zh/001-guide/002-advance/010-debug.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 调试

GemElement 是标准的自定义元素,能在浏览器 DevTools 中检查和调试。也可以安装 [Gem Devtools](https://chrome.google.com/webstore/detail/gem-devtools/lgfpciakeemopebkmjajengljoakjfle) 扩展,它能让你更直观的检查和修改 Gem 元素的数据。
GemElement 是标准的自定义元素,能在浏览器 DevTools 中检查和调试。也可以安装 [Gem DevTools](https://chrome.google.com/webstore/detail/gem-devtools/lgfpciakeemopebkmjajengljoakjfle) 扩展,它能让你更直观的检查和修改 Gem 元素的数据。

## 使用 Gem Devtools
## 使用 Gem DevTools

![Gem Devtools](https://raw.githubusercontent.com/mantou132/gem/master/packages/gem-devtools/screenshot/firefox.jpg)
![Gem DevTools](https://raw.githubusercontent.com/mantou132/gem/master/packages/gem-devtools/screenshot/firefox.jpg)
3 changes: 3 additions & 0 deletions packages/gem/docs/zh/004-blog/006-es-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ let MyElement = (() => {
## 使用 ES 装饰器的缺陷

`@attribute` 不再通过 `observedAttributes` 进行工作,而是拦截 `setAttribute`,在 DevTools 中修改时不使用修改后的 `setAttribute`,所以在 DevTools 中修改元素 Attribute 不能触发元素更新。

> [!NOTE]
> 安装浏览器扩展 [Gem DevTools](https://chrome.google.com/webstore/detail/gem-devtools/lgfpciakeemopebkmjajengljoakjfle) 将解决这一问题
66 changes: 30 additions & 36 deletions packages/gem/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,48 +62,42 @@ export function refobject<T extends GemElement<any>, V extends HTMLElement>(
});
}

type AttrType = BooleanConstructor | NumberConstructor | StringConstructor;
const observedTargetAttributes = new WeakMap<GemElementPrototype, Map<string, string>>();
function hackObservedAttribute(target: any, prop: string, attr: string) {
const attrMap = observedTargetAttributes.get(target) || new Map<string, string>();
attrMap.set(attr, prop);
if (!observedTargetAttributes.has(target)) {
const { setAttribute, toggleAttribute, removeAttribute } = Element.prototype;
target.setAttribute = function (n: string, v: string) {
const p = attrMap.get(n);
if (!p) return setAttribute.call(this, n, v);
this[p] = v;
};
target.removeAttribute = function (n: string) {
const p = attrMap.get(n);
if (!p) return removeAttribute.call(this, n);
this[p] = null;
};
target.toggleAttribute = function (n: string, force?: boolean) {
const p = attrMap.get(n);
if (!p) return toggleAttribute.call(this, n, force);
return (this[p] = force ?? !this.hasAttribute(n));
};
}
observedTargetAttributes.set(target, attrMap);
}

function defineAttr(t: GemElement, prop: string, attr: string, attrType: AttrType) {
const target = Object.getPrototypeOf(t);
if (!target.hasOwnProperty(prop)) {
pushStaticField(target, 'observedAttributes', attr); // 没有 observe 的效果
defineProperty(target, prop, { attr, attrType });
// 不在 Devtools 中工作 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#dom_access
hackObservedAttribute(target, prop, attr);
}
clearField(t, prop);
}
// hack 修改 attribute 行为,如果是观察的,就使用 `setter`
// 不在 Devtools 中工作 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#dom_access
// 使用 Gem DevTools 注入脚本监听 attribute 变化
const { setAttribute, toggleAttribute, removeAttribute } = Element.prototype;
GemElement.prototype.setAttribute = function (n: string, v: string) {
const prop = observedTargetAttributes.get(Object.getPrototypeOf(this))?.get(n);
if (!prop) return setAttribute.call(this, n, v);
(this as any)[prop] = v;
};
GemElement.prototype.removeAttribute = function (n: string) {
const prop = observedTargetAttributes.get(Object.getPrototypeOf(this))?.get(n);
if (!prop) return removeAttribute.call(this, n);
(this as any)[prop] = null;
};
GemElement.prototype.toggleAttribute = function (n: string, force?: boolean) {
const prop = observedTargetAttributes.get(Object.getPrototypeOf(this))?.get(n);
if (!prop) return toggleAttribute.call(this, n, force);
return ((this as any)[prop] = force ?? !this.hasAttribute(n));
};

type AttrType = BooleanConstructor | NumberConstructor | StringConstructor;
function decoratorAttr<T extends GemElement<any>>(context: ClassFieldDecoratorContext<T>, attrType: AttrType) {
const prop = context.name as string;
const attr = camelToKebabCase(prop);
context.addInitializer(function (this: T) {
defineAttr(this, prop, attr, attrType);
const target = Object.getPrototypeOf(this);
if (!target.hasOwnProperty(prop)) {
pushStaticField(target, 'observedAttributes', attr); // 没有 observe 的效果
defineProperty(target, prop, { attr, attrType });
// 记录观察的 attribute
const attrMap = observedTargetAttributes.get(target) || new Map<string, string>();
attrMap.set(attr, prop);
observedTargetAttributes.set(target, attrMap);
}
clearField(this, prop);
});
// 延时定义的元素需要继承原实例属性值
return function (this: any, initValue: any) {
Expand Down

0 comments on commit 2c8f7d5

Please sign in to comment.