1class DwComponentLoader { 2 #spinner; 3 #spinnerActiveClass; 4 5 constructor(parameters) { 6 if (!parameters) { 7 throw new Error("Cannot create loader without parameters"); 8 } 9 10 if (!parameters.spinnerSelector) { 11 throw new Error("Cannot create loader without spinnerSelector in parameters"); 12 } 13 14 if (!parameters.spinnerActiveClass) { 15 throw new Error("Cannot create loader without spinnerActiveClass in parameters"); 16 } 17 18 this.#spinner = document.querySelector(parameters.spinnerSelector); 19 20 if (!this.#spinner) { 21 throw new Error(`Cannot find spinner element: ${parameters.spinnerSelector}`); 22 } 23 24 this.#spinnerActiveClass = parameters.spinnerActiveClass; 25 } 26 27 async load(request) { 28 if (!(request instanceof Request)) { 29 throw new Error("Cannot load component without Request object"); 30 } 31 32 this.#showSpinner(); 33 34 try { 35 const response = await this.#sendRequest(request); 36 const data = await this.#parseResponse(response); 37 await this.#loadAssets(data); 38 this.#insert(data); 39 } finally { 40 this.#hideSpinner(); 41 } 42 } 43 44 async #sendRequest(request) { 45 const response = await fetch(request); 46 if (!response.ok) { 47 throw { 48 message: `Cannot fetch component: HTTP ${response.status}`, 49 reasons: [`HTTP error: ${response.status}`], 50 status: response.status 51 }; 52 } 53 54 return response; 55 } 56 57 async #parseResponse(response) { 58 const result = await response.json(); 59 if (result.errors && result.errors.length > 0) { 60 throw { 61 message: "Cannot load component: API returned errors", 62 reasons: result.errors, 63 response: result 64 }; 65 } 66 67 if (result.status !== "success" || !result.data) { 68 throw { 69 message: "Cannot parse response: invalid format", 70 reasons: ["Invalid response format"], 71 response: result 72 }; 73 } 74 75 if (!result.data.html) { 76 throw { 77 message: "Cannot render component: no HTML content", 78 reasons: ["Missing HTML content in response"], 79 response: result 80 }; 81 } 82 83 return result.data; 84 } 85 86 async #loadAssets(data) { 87 if (!data.assets) { 88 return; 89 } 90 91 const jsPaths = Array.isArray(data?.assets?.js) ? data.assets.js : []; 92 const cssPaths = Array.isArray(data?.assets?.css) ? data.assets.css : []; 93 94 await Promise.all([this.#loadScripts(...jsPaths), this.#loadStyles(...cssPaths)]); 95 } 96 97 async #loadScripts(...paths) { 98 const promises = paths.map((path) => this.#loadScript(path)); 99 return Promise.all(promises); 100 } 101 102 async #loadStyles(...paths) { 103 const promises = paths.map((path) => this.#loadStyle(path)); 104 return Promise.all(promises); 105 } 106 107 #loadScript(path) { 108 if (this.#hasScript(path)) { 109 return Promise.resolve(); 110 } 111 112 return new Promise((resolve, reject) => { 113 const script = document.createElement("script"); 114 script.src = path; 115 script.onload = resolve; 116 script.onerror = () => 117 reject({ 118 message: `Cannot load script: ${path}`, 119 reasons: [`Failed to load script: ${path}`] 120 }); 121 122 document.head.appendChild(script); 123 }); 124 } 125 126 #loadStyle(path) { 127 if (this.#hasStyle(path)) { 128 return Promise.resolve(); 129 } 130 131 return new Promise((resolve, reject) => { 132 const link = document.createElement("link"); 133 link.rel = "stylesheet"; 134 link.href = path; 135 link.onload = resolve; 136 link.onerror = () => 137 reject({ 138 message: `Cannot load stylesheet: ${path}`, 139 reasons: [`Failed to load stylesheet: ${path}`] 140 }); 141 142 document.head.appendChild(link); 143 }); 144 } 145 146 #hasScript(path) { 147 return document.querySelector(`script[src="${path}"]`) !== null; 148 } 149 150 #hasStyle(path) { 151 return document.querySelector(`link[href="${path}"]`) !== null; 152 } 153 154 #insert(data) { 155 const fragment = document.createRange().createContextualFragment(data.html); 156 157 document.body.appendChild(fragment); 158 } 159 160 #showSpinner() { 161 this.#spinner.classList.add(this.#spinnerActiveClass); 162 } 163 164 #hideSpinner() { 165 this.#spinner.classList.remove(this.#spinnerActiveClass); 166 } 167}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.