diff --git a/assets/css/litespeed.css b/assets/css/litespeed.css
index 7ddb5e910..7f4c0c36c 100644
--- a/assets/css/litespeed.css
+++ b/assets/css/litespeed.css
@@ -2454,6 +2454,49 @@ g.litespeed-pie_info .litespeed-pie-done {
}
}
+/* Shared "Default values" panel widgets (recommended() helper + CDN React component). */
+.litespeed-defaults-flags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 6px;
+ max-width: 320px;
+}
+
+.litespeed-defaults-flag {
+ display: inline-block;
+ font-family: Consolas, Menlo, Monaco, "Courier New", monospace;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 1px 6px;
+ border-radius: 3px;
+ border: 1px solid transparent;
+ line-height: 1.5;
+}
+
+.litespeed-defaults-flag--missing {
+ background-color: #fde2e2;
+ color: #8a1f11;
+ border-color: #f1a5a5;
+}
+
+.litespeed-defaults-flag--inconsistent {
+ background-color: #fff3cd;
+ color: #856404;
+ border-color: #ffe69c;
+}
+
+.litespeed-defaults-add-link {
+ display: inline-block;
+ margin-top: 6px;
+ font-size: 12px;
+ text-decoration: none;
+}
+
+.litespeed-defaults-add-link:hover {
+ text-decoration: underline;
+}
+
/* =======================================
VIEW - crawler
======================================= */
diff --git a/assets/js/component.cdn.js b/assets/js/component.cdn.js
index 55b2be7a9..64f16ff1b 100644
--- a/assets/js/component.cdn.js
+++ b/assets/js/component.cdn.js
@@ -2,6 +2,27 @@
* CDN module
* @author Hai Zheng
*/
+
+// Extensions auto-injected into the filetype textarea when their toggle is switched ON.
+// Switching OFF intentionally does NOT remove — the user must remove manually.
+const LITESPEED_CDN_TOGGLE_AUTOADD = {
+ inc_css: ['.css', '.less'],
+ inc_js: ['.js'],
+};
+
+// Reverse map used to flag extensions in the filetype list whose corresponding toggle is OFF.
+const LITESPEED_CDN_EXT_TO_TOGGLE = {
+ '.css': 'inc_css', '.less': 'inc_css',
+ '.js': 'inc_js',
+ '.gif': 'inc_img', '.jpeg': 'inc_img', '.jpg': 'inc_img',
+ '.png': 'inc_img', '.svg': 'inc_img', '.webp': 'inc_img',
+ '.pdf': 'inc_docs',
+ '.eot': 'inc_fonts', '.otf': 'inc_fonts', '.ttf': 'inc_fonts',
+ '.woff': 'inc_fonts', '.woff2': 'inc_fonts',
+ '.aac': 'inc_media', '.mp3': 'inc_media',
+ '.mp4': 'inc_media', '.ogg': 'inc_media',
+};
+
class CDNMapping extends React.Component {
constructor(props) {
super(props);
@@ -18,7 +39,22 @@ class CDNMapping extends React.Component {
const target = e.currentTarget;
const value = target.dataset.hasOwnProperty('value') ? Boolean(target.dataset.value * 1) : target.value;
const list = this.state.list;
- list[index][target.dataset.type] = value;
+ const type = target.dataset.type;
+ list[index][type] = value;
+
+ // When inc_css / inc_js is switched ON, auto-append its file extensions to the filetype textarea
+ // so the toggle works for non-enqueued references too. Toggling OFF never removes — that stays manual.
+ if (value === true && LITESPEED_CDN_TOGGLE_AUTOADD[type]) {
+ const raw = list[index].filetype;
+ const current = (raw ? (Array.isArray(raw) ? raw.slice() : String(raw).split('\n')) : [])
+ .map((s) => s.trim())
+ .filter(Boolean);
+ const lower = new Set(current.map((s) => s.toLowerCase()));
+ const toAdd = LITESPEED_CDN_TOGGLE_AUTOADD[type].filter((ext) => !lower.has(ext.toLowerCase()));
+ if (toAdd.length > 0) {
+ list[index].filetype = current.concat(toAdd).join('\n');
+ }
+ }
this.setState({
list: list,
@@ -55,13 +91,14 @@ class CDNMapping extends React.Component {
}
}
-// { url: '', inc_img: true, inc_css: false, inc_js: false, filetype: [ '.aac', '.eot', ... ] }
+// { url: '', inc_img: true, inc_css: false, inc_js: false, inc_docs: true, inc_fonts: true, inc_media: true, filetype: [ '.aac', '.eot', ... ] }
class CDNMappingBlock extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.delRow = this.delRow.bind(this);
+ this.addMissingDefaults = this.addMissingDefaults.bind(this);
}
onChange(e) {
@@ -72,12 +109,71 @@ class CDNMappingBlock extends React.Component {
this.props.delRow(this.props.index);
}
+ // Split a filetype value (array or newline string) into a clean array of extensions.
+ splitFiletype(value) {
+ if (!value) return [];
+ const arr = Array.isArray(value) ? value : String(value).split('\n');
+ return arr.map((s) => s.trim()).filter(Boolean);
+ }
+
+ // Defaults from the localize bundle that aren't in the user's current saved list (case-insensitive).
+ getMissingDefaults() {
+ const defaults = (litespeed_data && litespeed_data['cdn_mapping_filetype_default']) || [];
+ const current = this.splitFiletype(this.props.item.filetype);
+ const currentSet = new Set(current.map((s) => s.toLowerCase()));
+ return defaults.filter((ext) => !currentSet.has(String(ext).trim().toLowerCase()));
+ }
+
+ // Extensions that ARE in the current list but whose corresponding toggle is OFF — the generic
+ // file-type rewriter would still send them through the CDN, contradicting the toggle.
+ getInconsistent() {
+ const item = this.props.item;
+ const current = this.splitFiletype(item.filetype);
+ const seen = new Set();
+ const result = [];
+ for (const ext of current) {
+ const key = ext.toLowerCase();
+ if (seen.has(key)) continue;
+ seen.add(key);
+ const toggle = LITESPEED_CDN_EXT_TO_TOGGLE[key];
+ if (toggle && !item[toggle]) {
+ result.push(ext);
+ }
+ }
+ return result;
+ }
+
+ addMissingDefaults() {
+ const missing = this.getMissingDefaults();
+ if (missing.length === 0) return;
+
+ const current = this.splitFiletype(this.props.item.filetype);
+ const newValue = current.concat(missing).join('\n');
+
+ // Reuse the existing change pipeline by synthesising a textarea-style event.
+ this.props.onChange(
+ { currentTarget: { value: newValue, dataset: { type: 'filetype' } } },
+ this.props.index
+ );
+ }
+
render() {
const name_prefix = litespeed_data['ids']['cdn_mapping'];
const item = this.props.item;
const filetype = item.filetype ? (Array.isArray(item.filetype) ? item.filetype.join('\n') : item.filetype) : '';
+ const defaults = (litespeed_data && litespeed_data['cdn_mapping_filetype_default']) || [];
+ const missing = this.getMissingDefaults();
+ const inconsistent = this.getInconsistent();
+
+ // Size the readonly defaults textarea to mirror PHP's `recommended()` helper.
+ const defaultsRows = Math.min(Math.max(defaults.length + 1, 5), 40);
+ let defaultsCols = 30;
+ for (const v of defaults) {
+ if (String(v).length > defaultsCols) defaultsCols = String(v).length;
+ }
+ defaultsCols = Math.min(defaultsCols, 150);
return (
@@ -151,6 +247,60 @@ class CDNMappingBlock extends React.Component {
+
+
{litespeed_data['lang']['cdn_mapping_inc_docs']}
+
+
+
+
+
+
+
+
+
+
+
{litespeed_data['lang']['cdn_mapping_inc_fonts']}
+
+
+
+
+
+
+
+
+
+
+
{litespeed_data['lang']['cdn_mapping_inc_media']}
+
+
+
+
+
+
+
+
+
@@ -163,6 +313,45 @@ class CDNMappingBlock extends React.Component {
+ {defaults.length > 0 && (
+
+
{litespeed_data['lang']['default_value']}:
+
+ {(missing.length > 0 || inconsistent.length > 0) && (
+
+ {missing.map((ext) => (
+
+ {ext}
+
+ ))}
+ {inconsistent.map((ext) => (
+
+ {ext}
+
+ ))}
+
+ )}
+ {missing.length > 0 && (
+
+ )}
+
+ )}
+