UNPKG

react-sass-inlinesvg

Version:
360 lines (359 loc) 15.7 kB
"use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.updateTextNode = exports.renderStoryCatalog = exports.setup = exports.ForTest = exports.animationNamePrefix = void 0; const react_1 = __importDefault(require("react")); const p_settle_1 = __importDefault(require("p-settle")); const react_use_1 = require("react-use"); const react_inlinesvg_1 = require("./react-inlinesvg"); exports.animationNamePrefix = "svg_"; const State = { cacheObject: {}, updateQueueObject: {}, aggregation: { start: { queue: [] }, fetch: { queue: [] }, render: { queue: [] }, }, setupCompleted: false, }; class ForTest { } exports.ForTest = ForTest; ForTest.state = State; function setup(pathMap, options = {}) { return { SVG: react_1.default.memo(createSvg(pathMap, options)), pathMap, }; } exports.setup = setup; function renderStoryCatalog(SVG, pathMap, className, useDefault = false) { return (react_1.default.createElement("div", { className: className }, Object.keys(pathMap).map((svgName) => (react_1.default.createElement("section", { key: svgName, "data-svg-name": svgName }, react_1.default.createElement("h1", null, svgName), react_1.default.createElement(SVG, { defaultName: useDefault ? svgName : undefined })))))); } exports.renderStoryCatalog = renderStoryCatalog; function createSvg(pathMap, { fetchOptions, uniquifyIDs, uniqueHash } = {}) { const options = { fetchOptions, uniquifyIDs, uniqueHash: uniqueHash || (0, react_inlinesvg_1.randomString)(8), }; const Component = (_a) => { var { onLoad, onError, innerRef, defaultName, title, description } = _a, props = __rest(_a, ["onLoad", "onError", "innerRef", "defaultName", "title", "description"]); const elementRef = react_1.default.useRef(null); const propsRef = react_1.default.useRef(null); const [defaultNameEnabled] = react_1.default.useState(() => Boolean(defaultName)); const svgName = useSvgName(pathMap, defaultName, elementRef, propsRef, options); (0, react_use_1.useIsomorphicLayoutEffect)(() => { const initializing = !propsRef.current; propsRef.current = { title, description, onError, onLoad }; // 初回描画時は処理が不要であり、軽量化するためにスキップ if (!initializing && elementRef.current) { updateTextNode(elementRef.current, { title, description }); } }, [onLoad, onError, title, description]); (0, react_use_1.useIsomorphicLayoutEffect)(() => { const element = elementRef.current; if (!element || !defaultName || svgNameIsEmptyType(defaultName)) { return; } updateElement(element, defaultName, pathMap, propsRef, options); }, []); (0, react_use_1.useEffectOnce)(() => { // 一番最初にここへ到達した処理のみが実施する初期処理 if (!State.setupCompleted) { setupStyle(Object.keys(pathMap)); State.setupCompleted = true; } if (defaultNameEnabled) { return; } // 初回のsvg反映処理の開始 if (elementRef.current) { aggregateProcess(State.aggregation.start, { element: elementRef.current }, (list) => { list.map(({ element }) => { element.dataset.svgStatus = "loading"; }); }); } }); if (svgName === "NULL") { return null; } // スケルトンを描画 return (react_1.default.createElement("svg", Object.assign({}, resolveBaseProps(svgName), props, { ref: (ref) => { elementRef.current = ref; if (innerRef instanceof Function) { innerRef(ref); } else if (innerRef) { //eslint-disable-next-line @typescript-eslint/ban-ts-comment // 外部からrefが渡されている場合にreadonlyを無視して書き換える必要がある Object.assign(innerRef, { current: ref }); } } }))); }; Component.displayName = "Svg"; return Component; } function resolveBaseProps(svgName) { if (svgNameIsEmptyType(svgName)) { return { "data-svg-name": svgName, "data-svg-status": "complete", }; } return { "aria-busy": true, }; } function useSvgName(pathMap, defaultName, elementRef, propsRef, options) { const [svgName, setSvgName] = react_1.default.useState(defaultName); (0, react_use_1.useEffectOnce)(() => { const element = elementRef.current; if (element) { const handler = (event) => { // svg_SvgName を含む形式のアニメーション名をsvg名として検出 const svgName = resolveSvgName(event); // 特定のアニメーション名の形式に該当しない場合はその他のアニメーションなので無視する if (!svgName) { return; } event.stopPropagation(); if (svgName === "NULL") { setSvgName(svgName); } else { updateElement(element, svgName, pathMap, propsRef, options); } }; element.addEventListener("animationstart", handler, true); return () => { element.removeEventListener("animationstart", handler, true); }; } }); return svgName; } function updateElement(element, svgName, pathMap, propsRef, options) { if (svgNameIsEmptyType(svgName)) { updateElementForEmpty(element, svgName); return; } if (svgName in State.cacheObject) { updateElementByCache(element, svgName, propsRef, options, pathMap[svgName]()); return; } updateElementByFetch(element, svgName, pathMap, propsRef, options); } function updateElementByFetch(element, svgName, pathMap, propsRef, options) { var _a; const onError = ((_a = propsRef.current) === null || _a === void 0 ? void 0 : _a.onError) || null; if (!(svgName in pathMap)) { onError === null || onError === void 0 ? void 0 : onError(new TypeError(`unknown svgName "${svgName}"`)); updateElementForError(element, svgName); return; } updateElementForLoading(element, svgName); if (svgName in State.updateQueueObject) { State.updateQueueObject[svgName].push(element); return; } State.updateQueueObject[svgName] = [element]; // 取得処理は一定数まとめたほうが処理時間を短縮できる様子 aggregateProcess(State.aggregation.fetch, { svgName, element, propsRef }, (list) => { (0, p_settle_1.default)(list.map(({ svgName }) => createFetch(pathMap[svgName](), options))) .then((results) => { results.map((result, index) => { var _a; if (result.isRejected) { const propsRef = list[index].propsRef; const onError = (_a = propsRef.current) === null || _a === void 0 ? void 0 : _a.onError; // TODO: State.updateQueueObject に溜まっているものにも反映する if (result.reason instanceof Error) { onError === null || onError === void 0 ? void 0 : onError(result.reason); } else { onError === null || onError === void 0 ? void 0 : onError(new TypeError("pSettle rejected")); } return; } const svgName = list[index].svgName; const src = pathMap[svgName](); State.cacheObject[svgName] = parse(result.value); State.updateQueueObject[svgName].forEach((element, index) => { const hasCache = index !== 0; updateElementByCache(element, svgName, propsRef, options, src, hasCache); }); State.updateQueueObject[svgName] = []; }); }) .catch((error) => { if (error instanceof Error) { onError === null || onError === void 0 ? void 0 : onError(error); } }); }); } function createFetch(url, options) { return fetch(url, options.fetchOptions).then(react_inlinesvg_1.responseHandler); } function updateElementForEmpty(element, svgName) { const render = () => { resetAttributes(element); element.dataset.svgName = svgName; element.dataset.svgStatus = "complete"; element.removeAttribute("aria-busy"); element.innerHTML = ""; }; aggregateProcess(State.aggregation.render, { render }, (list) => { requestAnimationFrame(() => { list.forEach(({ render }) => render()); }); }, 16, 32); } function resetAttributes(element) { // 前回のsvgの属性を削除 if (element.dataset.attributeNames) { element.dataset.attributeNames.split(" ").forEach((name) => { element.removeAttribute(name); }); element.removeAttribute("data-attribute-names"); } } function updateElementForLoading(element, svgName) { resetAttributes(element); element.dataset.svgName = svgName; element.dataset.svgStatus = "loading"; element.setAttribute("aria-busy", "true"); element.innerHTML = ""; } function updateElementForError(element, svgName) { element.dataset.svgName = svgName; element.dataset.svgStatus = "error"; element.removeAttribute("aria-busy"); } function updateElementByCache(element, svgName, propsRef, options, src, hasCache = true) { const render = () => { var _a, _b; const { attributes, content } = State.cacheObject[svgName]; const attributeNames = []; resetAttributes(element); attributes.forEach(({ name, value }) => { element.setAttribute(name, value); attributeNames.push(name); }); element.dataset.svgName = svgName; element.dataset.svgStatus = "complete"; element.dataset.attributeNames = attributeNames.join(" "); // 今回のsvgの属性を登録 element.removeAttribute("aria-busy"); element.innerHTML = content; propsRef.current && updateTextNode(element, propsRef.current); (0, react_inlinesvg_1.updateSVGAttributes)(element, { baseURL: "", uniquifyIDs: options.uniquifyIDs, hash: options.uniqueHash, }); (_b = (_a = propsRef.current) === null || _a === void 0 ? void 0 : _a.onLoad) === null || _b === void 0 ? void 0 : _b.call(_a, src, hasCache); }; aggregateProcess(State.aggregation.render, { render }, (list) => { requestAnimationFrame(() => { list.forEach(({ render }) => render()); }); }, 16, 32); } function updateTextNode(svg, { title, description }) { var _a, _b; const svgName = svg.dataset.svgName; if (svgNameIsEmptyType(svgName)) { return; } if (description) { (_a = svg.querySelector("desc")) === null || _a === void 0 ? void 0 : _a.remove(); const descElement = document.createElement("desc"); descElement.textContent = description; svg.prepend(descElement); } if (title) { (_b = svg.querySelector("title")) === null || _b === void 0 ? void 0 : _b.remove(); const titleElement = document.createElement("title"); titleElement.textContent = title; svg.prepend(titleElement); } } exports.updateTextNode = updateTextNode; function aggregateProcess(state, data, run, delayMs = 16, unitSize = 16) { // 処理が集中したとき、全てをキューに追加し、1つ目のプロセスのみが非同期で一括でキューの処理を捌いていく。 // 指定されたdelayMsとunitSizeでキューの中身を処理するため、キューが空になる前に追加された分も順番に処理されていく。 // キューの処理を全て実行し終えたらキューを空にして再び元の状態に戻る。 state.queue.push(data); if (state.queue.length === 1) { // 次の処理の予約 function reserve(timerMs, pos = 0) { setTimeout(() => { const nextPos = pos + unitSize; run(state.queue.slice(pos, nextPos)); if (state.queue.length <= nextPos) { state.queue = []; } else { reserve(delayMs, nextPos); } }, timerMs); } reserve(0); } } function parse(text) { const svgStartMaker = "<svg "; // A<svg attr1="value1" attr2="value2">B</svg>C -> <svg attr="value">B const svgHtml = text.slice(text.indexOf(svgStartMaker), text.lastIndexOf("</svg>")); const gtPos = svgHtml.indexOf(">"); // <svg attr="value">B -> attr1="value1" attr2="value2" const attributesText = svgHtml.slice(svgStartMaker.length, gtPos); // <svg attr="value">B -> B const content = svgHtml.slice(gtPos + 1); const attributes = []; attributesText.replace(/([\d:a-z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi, (_, name, value, value2) => { attributes.push({ name, value: value !== null && value !== void 0 ? value : value2 }); return ""; }); return { content, attributes }; } function setupStyle(svgNames) { var _a; const id = "svg-style-keyframes"; (_a = document.querySelector(`#${id}`)) === null || _a === void 0 ? void 0 : _a.remove(); // Storybookなどだと重複して追加されるのでそれを防ぐ // ヘッダにSVG切り替えイベント用のアニメーションを設置する const style = document.createElement("style"); style.id = id; // "NULL" "NONE" "HIDDEN"は非表示用の特別なキー style.textContent = ["NULL", "NONE", "HIDDEN", ...svgNames] .map((name) => `@keyframes svg_${name} {}`) .join("\n"); // NOTE: SafariはCSS側でanimation-nameプロパティが当たる前に@keyframesが用意されていないとanimationstartが発火しない。 // そのため、まず@keyframesを設置してから、確実に次の描画以降でanimation-nameプロパティが当たるようにタイミングを調整する必要があります。 document.head.append(style); } function resolveSvgName(event) { return (event.animationName.startsWith(exports.animationNamePrefix) && event.animationName.slice(exports.animationNamePrefix.length)); } function svgNameIsEmptyType(svgName) { return svgName === "NONE" || svgName === "HIDDEN"; }