mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 23:49:07 +00:00
feat: Add support for HTML fragments (#845)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
6681fc0085
commit
4dab940db8
26 changed files with 1225 additions and 246 deletions
|
@ -1,12 +1,12 @@
|
|||
"""All code related to management of component dependencies (JS and CSS scripts)"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import lru_cache
|
||||
from hashlib import md5
|
||||
from textwrap import dedent
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
|
@ -34,9 +34,8 @@ from django.urls import path, reverse
|
|||
from django.utils.decorators import sync_and_async_middleware
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
|
||||
import django_components.types as types
|
||||
from django_components.util.html import SoupNode
|
||||
from django_components.util.misc import _escape_js, get_import_path
|
||||
from django_components.util.misc import get_import_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -325,6 +324,9 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
|
|||
return HttpResponse(processed_html)
|
||||
```
|
||||
"""
|
||||
if type not in ("document", "fragment"):
|
||||
raise ValueError(f"Invalid type '{type}'")
|
||||
|
||||
is_safestring = isinstance(content, SafeString)
|
||||
|
||||
if isinstance(content, str):
|
||||
|
@ -335,18 +337,24 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
|
|||
content_, js_dependencies, css_dependencies = _process_dep_declarations(content_, type)
|
||||
|
||||
# Replace the placeholders with the actual content
|
||||
# If type == `document`, we insert the JS and CSS directly into the HTML,
|
||||
# where the placeholders were.
|
||||
# If type == `fragment`, we let the client-side manager load the JS and CSS,
|
||||
# and remove the placeholders.
|
||||
did_find_js_placeholder = False
|
||||
did_find_css_placeholder = False
|
||||
css_replacement = css_dependencies if type == "document" else b""
|
||||
js_replacement = js_dependencies if type == "document" else b""
|
||||
|
||||
def on_replace_match(match: "re.Match[bytes]") -> bytes:
|
||||
nonlocal did_find_css_placeholder
|
||||
nonlocal did_find_js_placeholder
|
||||
|
||||
if match[0] == CSS_PLACEHOLDER_BYTES:
|
||||
replacement = css_dependencies
|
||||
replacement = css_replacement
|
||||
did_find_css_placeholder = True
|
||||
elif match[0] == JS_PLACEHOLDER_BYTES:
|
||||
replacement = js_dependencies
|
||||
replacement = js_replacement
|
||||
did_find_js_placeholder = True
|
||||
else:
|
||||
raise RuntimeError(
|
||||
|
@ -370,6 +378,10 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
|
|||
if maybe_transformed is not None:
|
||||
content_ = maybe_transformed.encode()
|
||||
|
||||
# In case of a fragment, we only append the JS (actually JSON) to trigger the call of dependency-manager
|
||||
if type == "fragment":
|
||||
content_ += js_dependencies
|
||||
|
||||
# Return the same type as we were given
|
||||
output = content_.decode() if isinstance(content, str) else content_
|
||||
output = mark_safe(output) if is_safestring else output
|
||||
|
@ -505,7 +517,8 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
|
|||
|
||||
# Core scripts without which the rest wouldn't work
|
||||
core_script_tags = Media(
|
||||
js=[static("django_components/django_components.min.js")],
|
||||
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
|
||||
js=[static("django_components/django_components.min.js")] if type == "document" else [],
|
||||
).render_js()
|
||||
|
||||
final_script_tags = "".join(
|
||||
|
@ -514,7 +527,7 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
|
|||
*[tag for tag in core_script_tags],
|
||||
# Make calls to the JS dependency manager
|
||||
# Loads JS from `Media.js` and `Component.js` if fragment
|
||||
exec_script,
|
||||
*([exec_script] if exec_script else []),
|
||||
# JS from `Media.js`
|
||||
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
|
||||
# so the scripts are executed at proper order. In the dependency manager, we only mark those
|
||||
|
@ -620,7 +633,7 @@ def _prepare_tags_and_urls(
|
|||
to_load_js_urls.append(get_script_url("js", comp_cls))
|
||||
|
||||
if _is_nonempty_str(comp_cls.css):
|
||||
loaded_css_urls.append(get_script_url("css", comp_cls))
|
||||
to_load_css_urls.append(get_script_url("css", comp_cls))
|
||||
|
||||
return (
|
||||
to_load_js_urls,
|
||||
|
@ -650,9 +663,20 @@ def _get_script_tag(
|
|||
script = get_script_content(script_type, comp_cls)
|
||||
|
||||
if script_type == "js":
|
||||
return f"<script>{_escape_js(script)}</script>"
|
||||
if "</script" in script:
|
||||
raise RuntimeError(
|
||||
f"Content of `Component.js` for component '{comp_cls.__name__}' contains '</script>' end tag. "
|
||||
"This is not allowed, as it would break the HTML."
|
||||
)
|
||||
return f"<script>{script}</script>"
|
||||
|
||||
elif script_type == "css":
|
||||
if "</style" in script:
|
||||
raise RuntimeError(
|
||||
f"Content of `Component.css` for component '{comp_cls.__name__}' contains '</style>' end tag. "
|
||||
"This is not allowed, as it would break the HTML."
|
||||
)
|
||||
|
||||
return f"<style>{script}</style>"
|
||||
|
||||
return script
|
||||
|
@ -678,51 +702,33 @@ def _gen_exec_script(
|
|||
to_load_css_tags: List[str],
|
||||
loaded_js_urls: List[str],
|
||||
loaded_css_urls: List[str],
|
||||
) -> str:
|
||||
# Generate JS expression like so:
|
||||
# ```js
|
||||
# Promise.all([
|
||||
# Components.manager.loadJs('<script src="/abc/def1">...</script>'),
|
||||
# Components.manager.loadJs('<script src="/abc/def2">...</script>'),
|
||||
# Components.manager.loadCss('<link href="/abc/def3">'),
|
||||
# ]);
|
||||
# ```
|
||||
) -> Optional[str]:
|
||||
if not to_load_js_tags and not to_load_css_tags and not loaded_css_urls and not loaded_js_urls:
|
||||
return None
|
||||
|
||||
def map_to_base64(lst: List[str]) -> List[str]:
|
||||
return [base64.b64encode(tag.encode()).decode() for tag in lst]
|
||||
|
||||
# Generate JSON that will tell the JS dependency manager which JS and CSS to load
|
||||
#
|
||||
# or
|
||||
#
|
||||
# ```js
|
||||
# Components.manager.markScriptLoaded("css", "/abc/def1.css"),
|
||||
# Components.manager.markScriptLoaded("css", "/abc/def2.css"),
|
||||
# Components.manager.markScriptLoaded("js", "/abc/def3.js"),
|
||||
# ```
|
||||
#
|
||||
# NOTE: It would be better to pass only the URL itself for `loadJs/loadCss`, instead of a whole tag.
|
||||
# NOTE: It would be simpler to pass only the URL itself for `loadJs/loadCss`, instead of a whole tag.
|
||||
# But because we allow users to specify the Media class, and thus users can
|
||||
# configure how the `<link>` or `<script>` tags are rendered, we need pass the whole tag.
|
||||
escaped_to_load_js_tags = [_escape_js(tag, eval=False) for tag in to_load_js_tags]
|
||||
escaped_to_load_css_tags = [_escape_js(tag, eval=False) for tag in to_load_css_tags]
|
||||
#
|
||||
# NOTE 2: Convert to Base64 to avoid any issues with `</script>` tags in the content
|
||||
exec_script_data = {
|
||||
"loadedCssUrls": map_to_base64(loaded_css_urls),
|
||||
"loadedJsUrls": map_to_base64(loaded_js_urls),
|
||||
"toLoadCssTags": map_to_base64(to_load_css_tags),
|
||||
"toLoadJsTags": map_to_base64(to_load_js_tags),
|
||||
}
|
||||
|
||||
# Make JS array whose items are interpreted as JS statements (e.g. functions)
|
||||
def js_arr(lst: List) -> str:
|
||||
return "[" + ", ".join(lst) + "]"
|
||||
|
||||
# NOTE: Wrap the body in self-executing function to avoid polluting the global scope.
|
||||
exec_script: types.js = f"""
|
||||
(() => {{
|
||||
Components.manager._loadComponentScripts({{
|
||||
loadedCssUrls: {json.dumps(loaded_css_urls)},
|
||||
loadedJsUrls: {json.dumps(loaded_js_urls)},
|
||||
toLoadCssTags: {js_arr(escaped_to_load_css_tags)},
|
||||
toLoadJsTags: {js_arr(escaped_to_load_js_tags)},
|
||||
}});
|
||||
document.currentScript.remove();
|
||||
}})();
|
||||
"""
|
||||
|
||||
# NOTE: The exec script MUST be executed SYNC, so we MUST NOT put `type="module"`,
|
||||
# `async`, nor `defer` on it.
|
||||
# See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
exec_script = f"<script>{_escape_js(dedent(exec_script))}</script>"
|
||||
# NOTE: This data is embedded into the HTML as JSON. It is the responsibility of
|
||||
# the client-side code to detect that this script was inserted, and to load the
|
||||
# corresponding assets
|
||||
# See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#embedding_data_in_html
|
||||
exec_script = json.dumps(exec_script_data)
|
||||
exec_script = f'<script type="application/json" data-djc>{exec_script}</script>'
|
||||
return exec_script
|
||||
|
||||
|
||||
|
@ -807,8 +813,8 @@ def cached_script_view(
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
# E.g. `/components/cache/table.js/`
|
||||
path("cache/<str:comp_cls_hash>.<str:script_type>/", cached_script_view, name=CACHE_ENDPOINT_NAME),
|
||||
# E.g. `/components/cache/table.js`
|
||||
path("cache/<str:comp_cls_hash>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
(()=>{var x=Array.isArray,l=n=>typeof n=="function",w=n=>n!==null&&typeof n=="object",E=n=>(w(n)||l(n))&&l(n.then)&&l(n.catch);function j(n,a){try{return a?n.apply(null,a):n()}catch(r){S(r)}}function g(n,a){if(l(n)){let r=j(n,a);return r&&E(r)&&r.catch(i=>{S(i)}),[r]}if(x(n)){let r=[];for(let i=0;i<n.length;i++)r.push(g(n[i],a));return r}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof n}`)}function S(n){console.error(n)}var u=()=>{let n=new Set,a=new Set,r={},i={},b=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},M=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},y=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let o of t.attributes)e.setAttributeNode(o.cloneNode());return e},h=t=>{let e=b(t),o=e.getAttribute("src");if(!o||T("js",o))return;c("js",o);let s=y(e),p=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";s.async=p;let m=new Promise((d,f)=>{s.onload=()=>{d()},globalThis.document.body.append(s)});return{el:s,promise:m}},C=t=>{let e=M(t),o=e.getAttribute("href");if(!o||T("css",o))return;let s=y(e);return globalThis.document.head.append(s),c("css",o),{el:s,promise:Promise.resolve()}},c=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?n:a).add(e)},T=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?n:a).has(e)};return{callComponent:(t,e,o)=>{let s=r[t];if(!s)throw Error(`[Components] '${t}': No component registered for that name`);let p=Array.from(document.querySelectorAll(`[data-comp-id-${e}]`));if(!p.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let m=`${t}:${o}`,d=i[m];if(!d)throw Error(`[Components] '${t}': Cannot find input for hash '${o}'`);let f=d(),F={name:t,id:e,els:p},[L]=g(s,[f,F]);return L},registerComponent:(t,e)=>{r[t]=e},registerComponentData:(t,e,o)=>{let s=`${t}:${e}`;i[s]=o},loadJs:h,loadCss:C,markScriptLoaded:c,_loadComponentScripts:async t=>{t.loadedCssUrls.forEach(o=>c("css",o)),t.loadedJsUrls.forEach(o=>c("js",o)),Promise.all(t.toLoadCssTags.map(o=>C(o))).catch(console.error);let e=Promise.all(t.toLoadJsTags.map(o=>h(o))).catch(console.error)}}};var k={manager:u(),createComponentsManager:u,unescapeJs:r=>new DOMParser().parseFromString(r,"text/html").documentElement.textContent};globalThis.Components=k;})();
|
||||
(()=>{var x=o=>new DOMParser().parseFromString(o,"text/html").documentElement.textContent,E=Array.isArray,m=o=>typeof o=="function",H=o=>o!==null&&typeof o=="object",S=o=>(H(o)||m(o))&&m(o.then)&&m(o.catch);function N(o,i){try{return i?o.apply(null,i):o()}catch(s){L(s)}}function g(o,i){if(m(o)){let s=N(o,i);return s&&S(s)&&s.catch(c=>{L(c)}),[s]}if(E(o)){let s=[];for(let c=0;c<o.length;c++)s.push(g(o[c],i));return s}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof o}`)}function L(o){console.error(o)}var M=o=>{let i=new MutationObserver(s=>{for(let c of s)c.type==="childList"&&c.addedNodes.forEach(p=>{p.nodeName==="SCRIPT"&&p.hasAttribute("data-djc")&&o(p)})});return i.observe(document,{childList:!0,subtree:!0}),i};var y=()=>{let o=new Set,i=new Set,s={},c={},p=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},F=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},T=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let r of t.attributes)e.setAttributeNode(r.cloneNode());return e},f=t=>{let e=p(t),r=e.getAttribute("src");if(!r||C("js",r))return;d("js",r);let a=T(e),l=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";a.async=l;let u=new Promise((n,b)=>{a.onload=()=>{n()},globalThis.document.body.append(a)});return{el:a,promise:u}},h=t=>{let e=F(t),r=e.getAttribute("href");if(!r||C("css",r))return;let a=T(e);return globalThis.document.head.append(a),d("css",r),{el:a,promise:Promise.resolve()}},d=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?o:i).add(e)},C=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?o:i).has(e)},w=(t,e)=>{s[t]=e},j=(t,e,r)=>{let a=`${t}:${e}`;c[a]=r},A=(t,e,r)=>{let a=s[t];if(!a)throw Error(`[Components] '${t}': No component registered for that name`);let l=Array.from(document.querySelectorAll(`[data-comp-id-${e}]`));if(!l.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let u=`${t}:${r}`,n=c[u];if(!n)throw Error(`[Components] '${t}': Cannot find input for hash '${r}'`);let b=n(),v={name:t,id:e,els:l},[P]=g(a,[b,v]);return P},k=async t=>{let e=t.loadedCssUrls.map(n=>atob(n)),r=t.loadedJsUrls.map(n=>atob(n)),a=t.toLoadCssTags.map(n=>atob(n)),l=t.toLoadJsTags.map(n=>atob(n));e.forEach(n=>d("css",n)),r.forEach(n=>d("js",n)),Promise.all(a.map(n=>h(n))).catch(console.error);let u=Promise.all(l.map(n=>f(n))).catch(console.error)};return M(t=>{let e=JSON.parse(t.text);k(e)}),{callComponent:A,registerComponent:w,registerComponentData:j,loadJs:f,loadCss:h,markScriptLoaded:d}};var $={manager:y(),createComponentsManager:y,unescapeJs:x};globalThis.Components=$;})();
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import re
|
||||
from typing import Any, Callable, List, Optional, Type, TypeVar
|
||||
|
||||
from django.template.defaultfilters import escape
|
||||
|
||||
from django_components.util.nanoid import generate
|
||||
|
||||
T = TypeVar("T")
|
||||
|
@ -52,22 +50,6 @@ def get_import_path(cls_or_fn: Type[Any]) -> str:
|
|||
return module + "." + cls_or_fn.__qualname__
|
||||
|
||||
|
||||
# See https://stackoverflow.com/a/58800331/9788634
|
||||
# str.replace(/\\|`|\$/g, '\\$&');
|
||||
JS_STRING_LITERAL_SPECIAL_CHARS_REGEX = re.compile(r"\\|`|\$")
|
||||
|
||||
|
||||
# See https://stackoverflow.com/a/34064434/9788634
|
||||
def escape_js_string_literal(js: str) -> str:
|
||||
escaped_js = escape(js)
|
||||
|
||||
def on_replace_match(match: "re.Match[str]") -> str:
|
||||
return f"\\{match[0]}"
|
||||
|
||||
escaped_js = JS_STRING_LITERAL_SPECIAL_CHARS_REGEX.sub(on_replace_match, escaped_js)
|
||||
return escaped_js
|
||||
|
||||
|
||||
def default(val: Optional[T], default: T) -> T:
|
||||
return val if val is not None else default
|
||||
|
||||
|
@ -77,10 +59,3 @@ def get_last_index(lst: List, key: Callable[[Any], bool]) -> Optional[int]:
|
|||
if key(item):
|
||||
return len(lst) - 1 - index
|
||||
return None
|
||||
|
||||
|
||||
def _escape_js(js: str, eval: bool = True) -> str:
|
||||
escaped_js = escape_js_string_literal(js)
|
||||
# `unescapeJs` is the function we call in the browser to parse the escaped JS
|
||||
escaped_js = f"Components.unescapeJs(`{escaped_js}`)"
|
||||
return f"eval({escaped_js})" if eval else escaped_js
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
/** This file defines the API of the JS code. */
|
||||
import { createComponentsManager } from './manager';
|
||||
import { unescapeJs } from './utils';
|
||||
|
||||
export type * from './manager';
|
||||
|
||||
export const Components = (() => {
|
||||
const manager = createComponentsManager();
|
||||
|
||||
/** Unescape JS that was escaped in Django side with `escape_js` */
|
||||
const unescapeJs = (escapedJs: string) => {
|
||||
return new DOMParser().parseFromString(escapedJs, 'text/html').documentElement.textContent;
|
||||
};
|
||||
|
||||
return {
|
||||
manager,
|
||||
createComponentsManager,
|
||||
unescapeJs,
|
||||
};
|
||||
})();
|
||||
export const Components = {
|
||||
manager: createComponentsManager(),
|
||||
createComponentsManager,
|
||||
unescapeJs,
|
||||
};
|
||||
|
||||
// In browser, this is accessed as `Components.manager`, etc
|
||||
globalThis.Components = Components;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/** The actual code of the JS dependency manager */
|
||||
import { callWithAsyncErrorHandling } from './errorHandling';
|
||||
import { observeScriptTag } from './mutationObserver';
|
||||
import { unescapeJs } from './utils';
|
||||
|
||||
type MaybePromise<T> = Promise<T> | T;
|
||||
|
||||
|
@ -233,24 +235,35 @@ export const createComponentsManager = () => {
|
|||
toLoadCssTags: string[];
|
||||
toLoadJsTags: string[];
|
||||
}) => {
|
||||
const loadedCssUrls = inputs.loadedCssUrls.map((s) => atob(s));
|
||||
const loadedJsUrls = inputs.loadedJsUrls.map((s) => atob(s));
|
||||
const toLoadCssTags = inputs.toLoadCssTags.map((s) => atob(s));
|
||||
const toLoadJsTags = inputs.toLoadJsTags.map((s) => atob(s));
|
||||
|
||||
// Mark as loaded the CSS that WAS inlined into the HTML.
|
||||
inputs.loadedCssUrls.forEach((s) => markScriptLoaded("css", s));
|
||||
inputs.loadedJsUrls.forEach((s) => markScriptLoaded("js", s));
|
||||
loadedCssUrls.forEach((s) => markScriptLoaded("css", s));
|
||||
loadedJsUrls.forEach((s) => markScriptLoaded("js", s));
|
||||
|
||||
// Load CSS that was not inlined into the HTML
|
||||
// NOTE: We don't need to wait for CSS to load
|
||||
Promise
|
||||
.all(inputs.toLoadCssTags.map((s) => loadCss(s)))
|
||||
.all(toLoadCssTags.map((s) => loadCss(s)))
|
||||
.catch(console.error);
|
||||
|
||||
// Load JS that was not inlined into the HTML
|
||||
const jsScriptsPromise = Promise
|
||||
// NOTE: Interestingly enough, when we insert scripts into the DOM programmatically,
|
||||
// the order of execution is the same as the order of insertion.
|
||||
.all(inputs.toLoadJsTags.map((s) => loadJs(s)))
|
||||
.all(toLoadJsTags.map((s) => loadJs(s)))
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
// Initialise the MutationObserver that watches for `<script>` tags with `data-djc` attribute
|
||||
observeScriptTag((script) => {
|
||||
const data = JSON.parse(script.text);
|
||||
_loadComponentScripts(data);
|
||||
});
|
||||
|
||||
return {
|
||||
callComponent,
|
||||
registerComponent,
|
||||
|
@ -258,6 +271,5 @@ export const createComponentsManager = () => {
|
|||
loadJs,
|
||||
loadCss,
|
||||
markScriptLoaded,
|
||||
_loadComponentScripts,
|
||||
};
|
||||
};
|
||||
|
|
27
src/django_components_js/src/mutationObserver.ts
Normal file
27
src/django_components_js/src/mutationObserver.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/** Set up MutationObserver that watches for `<script>` tags with `data-djc` attribute */
|
||||
export const observeScriptTag = (onScriptTag: (node: HTMLScriptElement) => void) => {
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === "childList") {
|
||||
// Check added nodes
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (
|
||||
node.nodeName === "SCRIPT" &&
|
||||
(node as HTMLElement).hasAttribute("data-djc")
|
||||
) {
|
||||
onScriptTag(node as HTMLScriptElement);
|
||||
}
|
||||
});
|
||||
2;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Observe the entire document
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true, // To detect nodes added anywhere in the DOM
|
||||
});
|
||||
|
||||
return observer;
|
||||
};
|
|
@ -1,10 +1,19 @@
|
|||
// Helper functions taken from @vue/shared
|
||||
/** Unescape JS that was escaped in Django side with `escape_js` */
|
||||
export const unescapeJs = (escapedJs: string) => {
|
||||
const doc = new DOMParser().parseFromString(escapedJs, "text/html")
|
||||
return doc.documentElement.textContent as string;
|
||||
};
|
||||
|
||||
// ////////////////////////////////////////////////////////
|
||||
// Helper functions below were taken from @vue/shared
|
||||
// See https://github.com/vuejs/core/blob/91112520427ff55941a1c759d7d60a0811ff4a61/packages/shared/src/general.ts#L105
|
||||
// ////////////////////////////////////////////////////////
|
||||
|
||||
export const isArray = Array.isArray;
|
||||
export const isFunction = (val: unknown): val is Function => typeof val === 'function';
|
||||
export const isFunction = (val: unknown): val is Function =>
|
||||
typeof val === "function";
|
||||
export const isObject = (val: unknown): val is Record<any, any> => {
|
||||
return val !== null && typeof val === 'object';
|
||||
return val !== null && typeof val === "object";
|
||||
};
|
||||
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
|
||||
return (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue