itmar-block-packages
Version:
Common React components for WordPress custom blocks, now fully TypeScript-ready.
183 lines (180 loc) • 7.28 kB
JavaScript
import { jsx } from 'react/jsx-runtime';
import { ServerStyleSheet } from 'styled-components';
import { renderToString } from 'react-dom/server';
// 注入済みのクラス名を記憶する(関数の外で定義することで、呼び出し間で共有される)
const injectedClasses = new Set();
const resolveTarget = (el, selector = ".itmar-wrap", mode = "auto") => {
if (mode === "self") {
return el;
}
if (mode === "outer") {
const parent = el.parentElement;
return parent?.matches(selector) ? parent : null;
}
if (mode === "inner") {
return el.querySelector(selector);
}
// auto: 外側は直親だけ、内側は直近の子孫
const parent = el.parentElement;
if (parent?.matches(selector)) {
return parent;
}
return el.querySelector(selector) || el;
};
const styleComponentApply = (StyleComp, blockSelector, options = {}) => {
const blocks = document.querySelectorAll(blockSelector);
blocks.forEach((el) => {
const attrData = el.getAttribute("data-attributes");
if (!attrData)
return;
try {
const attributes = JSON.parse(attrData);
//styleタグの生成
const sheet = new ServerStyleSheet();
let html = "";
let styleTags = "";
try {
html = renderToString(sheet.collectStyles(jsx(StyleComp, { attributes: attributes })));
styleTags = sheet.getStyleTags();
}
finally {
sheet.seal();
}
// 正規表現でクラス名を抽出
const classMatch = html.match(/class="([^"]+)"/);
const className = classMatch ? classMatch[1] : "";
// ----------------------------------
if (className) {
// 3. 【重要】このクラス名がまだ注入されていなければ実行
if (!injectedClasses.has(className)) {
// 1. スタイルタグを <head> または要素の直前に注入
const styleContainer = document.createElement("div");
styleContainer.innerHTML = styleTags;
const styleElement = styleContainer.firstChild;
if (styleElement instanceof Node) {
document.head.appendChild(styleElement);
// 注入済みとして記録
injectedClasses.add(className);
}
}
// 2. クラス名を適用する対象を探す
// save.tsx で <div className="itmar-wrap"> のように目印をつけておくと確実です
const target = options.getTarget?.(el) ||
resolveTarget(el, options.selector ?? ".itmar-wrap", options.target ?? "auto");
if (!target)
return;
const classNames = className.trim().split(/\s+/).filter(Boolean);
target.classList.add(...classNames);
}
}
catch (e) {
console.error("Style injection failed:", e);
}
});
};
const stableSerialize = (value) => {
if (value === null || typeof value !== "object") {
return JSON.stringify(value) ?? "null";
}
if (Array.isArray(value)) {
return `[${value.map(stableSerialize).join(",")}]`;
}
const record = value;
const keys = Object.keys(record).sort();
return `{${keys
.map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`)
.join(",")}}`;
};
const hashString = (value) => {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0).toString(36);
};
/**
* React/styled-componentsを使わず、保存済み属性からスコープ付きCSSを適用する。
* 戻り値のrefreshはAjax等で追加されたブロックにも再適用できる。
*/
const styleDataApply = (createCss, blockSelector, options = {}) => {
const processedElements = new WeakSet();
const attributeName = options.attributeName ?? "data-attributes";
const classPrefix = options.classPrefix ?? "itmar-style-";
const applyToElement = (el) => {
if (processedElements.has(el))
return;
const attrData = el.getAttribute(attributeName);
if (!attrData)
return;
try {
const attributes = JSON.parse(attrData);
const styleKey = hashString(`${blockSelector}:${classPrefix}:${stableSerialize(attributes)}`);
const scopeClass = `${classPrefix}${styleKey}`;
const scopeSelector = `.${scopeClass}`;
const cssText = createCss(attributes, scopeSelector).trim();
const target = options.getTarget?.(el) ??
resolveTarget(el, options.selector ?? ".itmar-wrap", options.target ?? "auto");
if (!target)
return;
target.classList.add(scopeClass);
options.decorateTarget?.(target, attributes);
const existingStyle = document.head.querySelector(`style[data-itmar-style="${styleKey}"]`);
if (cssText && !existingStyle) {
const styleElement = document.createElement("style");
styleElement.dataset.itmarStyle = styleKey;
styleElement.textContent = cssText;
document.head.appendChild(styleElement);
}
processedElements.add(el);
}
catch (error) {
console.error("Style data injection failed:", error, el);
}
};
const refresh = (root = document) => {
if (root instanceof Element && root.matches(blockSelector)) {
applyToElement(root);
}
root.querySelectorAll(blockSelector).forEach(applyToElement);
};
const observer = options.observe === false
? null
: new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof Element)
refresh(node);
});
});
});
refresh();
observer?.observe(document.documentElement, {
childList: true,
subtree: true,
});
return {
refresh,
disconnect: () => observer?.disconnect(),
};
};
//純粋CSS生成関数
const cssValueToString = (value) => {
if (value == null || value === false)
return "";
if (Array.isArray(value)) {
return value.map(cssValueToString).join("");
}
if (typeof value === "object") {
return Object.entries(value)
.filter(([, cssValue]) => cssValue != null)
.map(([property, cssValue]) => {
const cssProperty = property.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
return `${cssProperty}:${cssValue};`;
})
.join("");
}
return String(value);
};
export { cssValueToString, styleComponentApply, styleDataApply };
//# sourceMappingURL=styleComponentApply.js.map