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']}:
+