MediaWiki:Gadget-PreloadEditor.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
!function(e,t){"use strict";const a={version:"1.2.0",state:{preloadTitle:"",preloadSource:"",preloadContent:"",config:null,variables:[],conditions:[],selectedVariable:"PAGE",deletedVariables:[],mode:"idle",dirty:!1,dirtyPreload:!1,dirtyConfig:!1,localDraftSavedAt:null,localDraftAvailable:!1},editor:null,editorView:null,analysis:null,root:null,workspace:null,status:null,localDraftStatus:null,editorTools:null,draftStorageKey:"PetitPlanet.PreloadEditor.drafts.v1",defaults:{preload:"<includeonly>\n<!-- preload here -->\n</includeonly>\n<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n"},
getDraftStorage(){try{const e=window.localStorage.getItem(this.draftStorageKey);if(!e)return{active:null,drafts:{}};const t=JSON.parse(e);return!t||"object"!=typeof t||Array.isArray(t)?{active:null,drafts:{}}:(t.drafts&&"object"==typeof t.drafts&&!Array.isArray(t.drafts)||(t.drafts={}),t)}catch(e){return console.warn("Preload Editor: Could not read local drafts.",e),{active:null,drafts:{}}}},
setDraftStorage(e){try{return window.localStorage.setItem(this.draftStorageKey,JSON.stringify(e)),!0}catch(e){return console.warn("Preload Editor: Could not save local draft.",e),!1}},
getDraftId:e=>(e=String(e||"").trim())?e.toLowerCase():"__untitled__",
getDraftSnapshot(){let e=this.getEditorContent();this.editorView||e||(e=this.state.preloadContent||"");let t=null;try{t=this.state.config?JSON.parse(JSON.stringify(this.state.config)):this.emptyConfig()}catch(e){console.warn("Preload Editor: Could not clone config for local draft.",e),t=this.emptyConfig()}return{version:4,preloadTitle:String(this.state.preloadTitle||""),preloadSource:String(this.state.preloadSource||""),preloadContent:String(e||""),config:t,variables:Array.isArray(this.state.variables)?this.state.variables.slice():[],conditions:Array.isArray(this.state.conditions)?this.state.conditions.slice():[],selectedVariable:String(this.state.selectedVariable||"PAGE"),deletedVariables:Array.isArray(this.state.deletedVariables)?this.state.deletedVariables.slice():[],mode:this.state.mode||"edit",dirty:!!this.state.dirty,dirtyPreload:!!this.state.dirtyPreload,dirtyConfig:!!this.state.dirtyConfig,savedAt:Date.now()}},
updateLocalDraftStatus(e,t){this.localDraftStatus&&(this.localDraftStatus.removeClass("is-saved","is-warning","is-error","is-empty"),t&&this.localDraftStatus.addClass("is-"+t),this.localDraftStatus.text(e||""))},
formatLocalDraftTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch(e){return""}},
updateLocalDraftIndicator(){if(this.localDraftStatus){if(this.state.localDraftSavedAt){const e=this.formatLocalDraftTime(this.state.localDraftSavedAt);return void this.updateLocalDraftStatus(e?"Saved locally at "+e:"Saved locally","saved")}this.updateLocalDraftStatus("No local draft","empty")}},
saveDraft(){if(!this.state.dirty)return!1;const e=this.getDraftSnapshot(),t=this.getDraftId(e.preloadTitle),a=this.getDraftStorage();a.drafts[t]=e,a.active=t;const i=this.setDraftStorage(a);return i?(this.state.localDraftSavedAt=e.savedAt,this.state.localDraftAvailable=!0,this.updateLocalDraftIndicator()):(this.state.localDraftAvailable=!1,this.updateLocalDraftStatus("Could not save local draft","error")),i},
clearDraft(e){const t=this.getDraftStorage(),a=this.getDraftId(e);t.drafts&&t.drafts[a]&&delete t.drafts[a],t.active===a&&(t.active=null),this.setDraftStorage(t),this.state.localDraftSavedAt=null,this.state.localDraftAvailable=!1,this.updateLocalDraftIndicator()},
getMostRecentDraft(){const e=this.getDraftStorage();if(!e.drafts||!Object.keys(e.drafts).length)return null;if(e.active&&e.drafts[e.active])return{id:e.active,draft:e.drafts[e.active]};let t=null,a=null;return Object.entries(e.drafts).forEach(([e,i])=>{i&&"object"==typeof i&&(!a||(Number(i.savedAt)||0)>(Number(a.savedAt)||0))&&(t=e,a=i)}),a?{id:t,draft:a}:null},
async restoreDraft(){const t=this.getMostRecentDraft();if(!t||!t.draft)return!1;const a=t.draft,i=String(a.preloadTitle||"").trim(),r=i||"an untitled preload",o=a.savedAt?new Date(a.savedAt).toLocaleString():"",s="Preload Editor found unsaved local work for "+r+(o?"\n\nLast saved locally: "+o:"")+"\n\nRestore it?";if(!await this.createConfirmDialog("Restore unsaved work",s,"Restore","Discard"))return this.clearDraft(i),!1;let n=this.emptyConfig();if(a.config&&"object"==typeof a.config&&!Array.isArray(a.config))try{n=this.normalizeConfig(JSON.parse(JSON.stringify(a.config)))}catch(e){console.warn("Preload Editor: Could not restore local config.",e)}const d="boolean"==typeof a.dirtyPreload||"boolean"==typeof a.dirtyConfig;return this.state={preloadTitle:i,preloadSource:String(a.preloadSource||""),preloadContent:String(a.preloadContent||""),config:n,variables:Array.isArray(a.variables)?a.variables.slice():[],conditions:Array.isArray(a.conditions)?a.conditions.slice():[],selectedVariable:String(a.selectedVariable||"PAGE"),deletedVariables:Array.isArray(a.deletedVariables)?a.deletedVariables.map(e=>String(e).toUpperCase()).filter(e=>"PAGE"!==e):[],mode:a.mode||"edit",dirty:!d||!(!a.dirtyPreload&&!a.dirtyConfig),dirtyPreload:!d||!!a.dirtyPreload,dirtyConfig:!d||!!a.dirtyConfig,localDraftSavedAt:Number(a.savedAt)||null,localDraftAvailable:!0},this.reanalyze(),this.renderEditor(),this.updateLocalDraftIndicator(),this.setStatus("Restored unsaved local work from this browser.","warning"),e.notify("Unsaved Preload Editor work restored.",{type:"success"}),!0},
async getPage(t){const a=await(new e.Api).get({action:"query",prop:"info|revisions",titles:t,rvprop:"content",rvslots:"main",formatversion:2}),i=a&&a.query&&a.query.pages&&a.query.pages[0];if(!i||i.missing)return null;const r=i.revisions&&i.revisions[0]&&i.revisions[0].slots&&i.revisions[0].slots.main&&i.revisions[0].slots.main.content;return{title:i.title||t,content:"string"==typeof r?r:"",contentmodel:i.contentmodel||"wikitext"}},
async pageExists(e){return!!await this.getPage(e)},
async searchPages(t){const a=await(new e.Api).get({action:"query",list:"search",srsearch:t,srlimit:10,srnamespace:10,formatversion:2});return a&&a.query&&a.query.search||[]},
async savePage(t,a,i,r,o){const s={action:"edit",title:t,text:a,summary:i||"Updated with Preload Editor",formatversion:2};return r&&(s.createonly=!0),o&&(s.contentmodel=o),(new e.Api).postWithToken("csrf",s)},
changeContentModel:async(t,a,i)=>(new e.Api).postWithToken("csrf",{action:"changecontentmodel",title:t,model:a,reason:i||"Changed content model with Preload Editor"}),
async saveConfigPage(e,t,a){const i=await this.getPage(e);return i?("json"!==String(i.contentmodel||"").toLowerCase()&&await this.changeContentModel(e,"json","Changed preload config content model to JSON"),this.savePage(e,t,a,!1)):this.savePage(e,t,a,!0,"json")},
normalizeConfigTitle:e=>(e=(e=String(e||"").trim()).replace(/\/+$/,""),/\/config\.json$/i.test(e)?e:e+"/config.json"),
cloneConfigField(e){try{return JSON.parse(JSON.stringify(e))}catch(e){return console.warn("Preload Editor: Could not clone imported field.",e),null}},
getImportableFields(e){const t=e&&e.fields;if(!t||"object"!=typeof t||Array.isArray(t))return{};const a={};return Object.entries(t).forEach(([e,t])=>{const i=String(e||"").trim().toUpperCase();if(!i||"PAGE"===i)return;if(!t||"object"!=typeof t||Array.isArray(t))return;const r=this.cloneConfigField(t);r&&(a[i]=r)}),a},
createImportDefinitionsDialog(e,a){return new Promise(i=>{const r=Object.keys(a).sort((e,t)=>e.localeCompare(t)),o=t("<div>").addClass("preload-editor-prompt-overlay preload-editor-import-overlay"),s=t("<div>").addClass("preload-editor-prompt preload-editor-import-dialog");if(t("<h2>").text("Import variable definitions").appendTo(s),t("<p>").text('Choose which variable definitions to import from "'+e+'". Existing local definitions can be replaced.').appendTo(s),!r.length){t("<div>").addClass("preload-editor-empty preload-editor-import-empty").text("This config.json does not contain any importable variable definitions.").appendTo(s);const e=t("<div>").addClass("preload-editor-prompt-buttons"),a=this.createButton("Close","preload-editor-button-primary",()=>{o.remove(),i(null)});return e.append(a),s.append(e),o.append(s),t(document.body).append(o),o.attr("tabindex","-1"),void o.trigger("focus")}const n=this.textInput("","Search variables...").addClass("preload-editor-import-search"),d=t("<div>").addClass("preload-editor-import-search-wrap").append(n);s.append(d);const l=t("<div>").addClass("preload-editor-import-controls"),c=this.createButton("Select all","",()=>{h.find('input[type="checkbox"]:visible').prop("checked",!0),g()}),p=this.createButton("Deselect all","",()=>{h.find('input[type="checkbox"]:visible').prop("checked",!1),g()});l.append(c,p),s.append(l);const h=t("<div>").addClass("preload-editor-import-list"),f=this.state.config&&this.state.config.fields?this.state.config.fields:{};r.forEach(e=>{const i=!!f[e],r=a[e],o=r&&r.type?String(r.type):"text",s=r&&r.label?String(r.label):this.humanizeName(e),n=t("<label>").addClass("preload-editor-import-row").attr("data-variable",e).attr("data-search",(e+" "+s+" "+o).toLowerCase()),d=this.checkbox(!1).attr("data-import-variable",e).addClass("preload-editor-import-checkbox"),l=t("<span>").addClass("preload-editor-import-content"),c=t("<span>").addClass("preload-editor-import-header");t("<strong>").text(e).appendTo(c),t("<span>").addClass("preload-editor-import-type").text(o).appendTo(c);const p=t("<span>").addClass("preload-editor-import-meta");t("<span>").text(s).appendTo(p);const u=t("<span>").addClass("preload-editor-import-status").toggleClass("is-existing",i).toggleClass("is-new",!i).text(i?"REPLACE EXISTING":"NEW");l.append(c,p,u),n.append(d,l),h.append(n)}),s.append(h);const u=t("<div>").addClass("preload-editor-import-summary"),g=()=>{const e=h.find('input[type="checkbox"]:checked').length,a=h.find('input[type="checkbox"]:checked').filter(function(){const e=t(this).attr("data-import-variable");return!!f[e]}).length;let i=e+" "+(1===e?"variable":"variables")+" selected.";a&&(i+=" "+a+" existing "+(1===a?"definition":"definitions")+" will be replaced."),u.text(i),m.prop("disabled",0===e)};n.on("input.preloadEditor",()=>{const e=String(n.val()||"").trim().toLowerCase();h.find(".preload-editor-import-row").each(function(){const a=t(this),i=String(a.attr("data-search")||"");a.toggle(!e||i.includes(e))})}),h.on("change.preloadEditor",'input[type="checkbox"]',g);const y=t("<div>").addClass("preload-editor-prompt-buttons"),v=this.createButton("Cancel","",()=>{o.remove(),i(null)}),m=this.createButton("Import selected","preload-editor-button-primary",()=>{const e=[];h.find('input[type="checkbox"]:checked').each(function(){e.push(String(t(this).attr("data-import-variable")))}),o.remove(),i(e)});y.append(v,m),s.append(u,y),o.append(s),t(document.body).append(o),o.attr("tabindex","-1"),o.trigger("focus"),g(),n.trigger("focus"),o.on("keydown.preloadEditor",e=>{"Escape"===e.key&&v.trigger("click")}),o.on("click.preloadEditor",e=>{e.target===o.get(0)&&v.trigger("click")})})},
async importVariableDefinitions(){const t=await this.createPromptDialog("Import variable definitions","Enter a preload title or its config.json title.");if(!t)return;const a=this.normalizeConfigTitle(t);this.setStatus("Loading source config...","loading");try{const t=await this.getPage(a);if(!t)throw new Error('No config.json was found at "'+a+'".');let i;try{i=JSON.parse(t.content)}catch(e){throw new Error("The source config.json contains invalid JSON: "+e.message)}if(!i||"object"!=typeof i||Array.isArray(i))throw new Error("The source config.json must contain a JSON object.");if(!i.fields||"object"!=typeof i.fields||Array.isArray(i.fields))throw new Error('The source config.json does not contain a valid "fields" object.');const r=this.getImportableFields(i),o=await this.createImportDefinitionsDialog(a,r);if(null===o)return void this.setStatus("Variable import cancelled.","warning");if(!o.length)return void this.setStatus("No variable definitions were imported.","warning");this.state.config||(this.state.config=this.emptyConfig()),this.state.config.fields||(this.state.config.fields={});const s=[],n=[];o.forEach(e=>{if("PAGE"===e)return;const t=r[e];if(!t)return;this.state.config.fields[e]?n.push(e):s.push(e);const a=this.cloneConfigField(t);a&&(this.state.config.fields[e]=a,Array.isArray(this.state.deletedVariables)&&(this.state.deletedVariables=this.state.deletedVariables.filter(t=>String(t).toUpperCase()!==e)))}),this.state.config.fields.PAGE=this.getAutomaticPageField(),this.markConfigDirty(),!!this.state.config.fields[this.state.selectedVariable]&&"PAGE"!==this.state.selectedVariable||(this.state.selectedVariable=o[0]||"PAGE"),this.reanalyze(),this.renderVariablesBySelector(),this.renderVariableConfigurationBySelector(),this.renderAnalysisBySelector(),this.updateEditorTools(),this.saveDraft();let d="Imported "+o.length+" variable "+(1===o.length?"definition":"definitions")+".";n.length&&(d+=" Replaced: "+n.join(", ")+"."),this.setStatus(d,"success"),e.notify(d,{type:"success"})}catch(t){console.error("Preload Editor variable import:",t),this.setStatus(t.message||"Could not import variable definitions.","error"),e.notify(t.message||"Could not import variable definitions.",{type:"error"})}},
extractIncludeonly(e){const t=String(e||"").match(/<includeonly>([\s\S]*?)<\/includeonly>/i);return t?t[1].trim():String(e||"").trim()},
findVariables(e){const t=[],a=/%%([A-Za-z0-9_-]+)%%/g;let i;for(;null!==(i=a.exec(e));){const e=i[1].toUpperCase();t.includes(e)||t.push(e)}return t},
findConditions(e){const t=[],a=/<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;let i;for(;null!==(i=a.exec(e));)t.push({type:i[1].toLowerCase(),expression:i[2].trim(),index:i.index});return t},
findConditionVariables(e){const t=[];return this.findConditions(e).forEach(e=>{if("if"!==e.type&&"elseif"!==e.type)return;(e.expression.match(/[A-Za-z0-9_-]+/g)||[]).forEach(e=>{e=e.toUpperCase(),t.includes(e)||t.push(e)})}),t},
analyzePreload(e){return{variables:this.findVariables(e),conditions:this.findConditions(e),conditionVariables:this.findConditionVariables(e)}},
getAutomaticPageField:()=>({label:"Page Name",description:"Name of the page.",type:"page",required:!0,namespace:"Main","allow-new":!0}),
emptyConfig(){return{settings:{incomplete:"add-stub"},fields:{PAGE:this.getAutomaticPageField()}}},
normalizeConfig(e){return e&&"object"==typeof e&&!Array.isArray(e)||(e=this.emptyConfig()),e.settings&&"object"==typeof e.settings&&!Array.isArray(e.settings)||(e.settings={}),e.fields&&"object"==typeof e.fields&&!Array.isArray(e.fields)||(e.fields={}),e.fields.PAGE=this.getAutomaticPageField(),e},
createDefaultField(e,t){return{label:this.humanizeName(e),description:"",type:t.conditionVariables.includes(e)?"boolean":"text"}},
humanizeName:e=>String(e||"").replace(/[_-]+/g," ").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/\s+/g," ").trim().replace(/\b\w/g,e=>e.toUpperCase()),
prepareConfigForVariables(e,t){const a=(e=this.normalizeConfig(e)).fields||{},i=new Set(Array.isArray(this.state.deletedVariables)?this.state.deletedVariables.map(e=>String(e).toUpperCase()):[]),r={};r.PAGE=this.getAutomaticPageField(),t.variables.forEach(e=>{"PAGE"===e||i.has(e)||(r[e]=a[e]||this.createDefaultField(e,t))}),Object.keys(a).forEach(e=>{const t=e.toUpperCase();"PAGE"===t||r[t]||i.has(t)||(r[t]=a[e])}),e.fields=r;return e},
validateConfig(e,t){const a=[],i=[];if(!e||"object"!=typeof e||Array.isArray(e))return{valid:!1,errors:["Config must be an object."],warnings:[]};if(!e.fields||"object"!=typeof e.fields||Array.isArray(e.fields))return{valid:!1,errors:['"fields" must be an object.'],warnings:i};const r=Object.keys(e.fields).map(e=>e.toUpperCase());t.variables.forEach(e=>{r.includes(e)||a.push('Variable "'+e+'" is not configured.')}),r.forEach(e=>{t.variables.includes(e)||i.push('Field "'+e+'" is not used by the preload.')}),r.includes("PAGE")||a.push("PAGE must always be configured."),Object.entries(e.fields).forEach(([e,t])=>{if(!t||"object"!=typeof t||Array.isArray(t))return void a.push('Field "'+e+'" must be an object.');t.type||i.push('Field "'+e+'" has no type.'),"select"!==t.type&&"multiselect"!==t.type||t.source||Array.isArray(t.options)||i.push('Field "'+e+'" is a '+t.type+" without a source or manual options.");if("file"===t.type){t.filename||a.push('File field "'+e+'" must define a filename template.');t.filetext===undefined&&i.push('File field "'+e+'" has no filetext; the uploaded File page will have an empty initial text.');t.accept!==undefined&&!Array.isArray(t.accept)&&a.push('File field "'+e+'" must have "accept" as an array.');t.accept&&Array.isArray(t.accept)&&t.accept.some(e=>!String(e).trim())&&a.push('File field "'+e+'" contains an empty accepted MIME type.')}}),{valid:!a.length,errors:a,warnings:i}},
transformationObject(e,t,a,i,r){if(!e)return null;switch(e){case"remove-prefix":return t?{transform:e,prefix:t}:null;case"remove-suffix":return a?{transform:e,suffix:a}:null;case"replace":return""===i?null:{transform:e,from:i,to:r||""};default:return null}},
createSection(e,a){const i=t("<section>").addClass("preload-editor-section");return a&&i.addClass(a),t("<h2>").addClass("preload-editor-section-title").text(e).appendTo(i),i},
createButton(e,a,i){const r=t("<button>").attr("type","button").addClass("preload-editor-button").text(e);return a&&r.addClass(a),i&&r.on("click.preloadEditor",i),r},
fieldRow(e,a,i){const r=t("<div>").addClass("preload-editor-field");return t("<label>").addClass("preload-editor-label").text(e).appendTo(r),i&&t("<div>").addClass("preload-editor-help").text(i).appendTo(r),r.append(a)},
textInput:(e,a)=>t("<input>").attr({type:"text",value:null==e?"":e,placeholder:a||""}).addClass("preload-editor-input"),
textarea:e=>t("<textarea>").val(null==e?"":e).addClass("preload-editor-textarea"),
checkbox:e=>t("<input>").attr("type","checkbox").prop("checked",!!e).addClass("preload-editor-checkbox"),
select(e,a){const i=t("<select>").addClass("preload-editor-select");return e.forEach(e=>{t("<option>").attr("value",e.value).text(e.label).prop("selected",String(e.value)===String(void 0===a?"":a)).appendTo(i)}),i},
createConfirmDialog(e,a,i,r){return new Promise(o=>{const s=t("<div>").addClass("preload-editor-prompt-overlay"),n=t("<div>").addClass("preload-editor-prompt");t("<h2>").text(e).appendTo(n);String(a||"").split("\n").forEach(e=>{""!==e?t("<p>").text(e).appendTo(n):t("<div>").addClass("preload-editor-prompt-spacer").appendTo(n)});const d=t("<div>").addClass("preload-editor-prompt-buttons"),l=this.createButton(r||"Cancel","",()=>{s.remove(),o(!1)}),c=this.createButton(i||"Confirm","preload-editor-button-primary",()=>{s.remove(),o(!0)});d.append(l,c),n.append(d),s.append(n),t(document.body).append(s),s.on("keydown.preloadEditor",e=>{"Escape"===e.key&&l.trigger("click")}),s.attr("tabindex","-1"),s.trigger("focus"),s.on("click.preloadEditor",e=>{e.target===s.get(0)&&l.trigger("click")})})},
createPromptDialog(e,a){return new Promise(i=>{const r=t("<div>").addClass("preload-editor-prompt-overlay"),o=t("<div>").addClass("preload-editor-prompt");t("<h2>").text(e).appendTo(o),t("<p>").text(a).appendTo(o);const s=this.textInput(),n=t("<div>").addClass("preload-editor-prompt-buttons"),d=this.createButton("Cancel","",()=>{r.remove(),i(null)}),l=this.createButton("Load","preload-editor-button-primary",()=>{const e=String(s.val()||"").trim();e&&(r.remove(),i(e))});n.append(d,l),o.append(s,n),r.append(o),t(document.body).append(r),s.trigger("focus"),s.on("keydown.preloadEditor",e=>{"Enter"===e.key&&l.trigger("click"),"Escape"===e.key&&d.trigger("click")})})},
createConfigReviewDialog(){return new Promise(e=>{const a=t("<div>").addClass("preload-editor-prompt-overlay preload-editor-code-review-overlay"),i=t("<div>").addClass("preload-editor-prompt preload-editor-code-review");t("<h2>").text("Review config.json").appendTo(i),t("<p>").text("This is the config.json that will be saved for the current preload.").appendTo(i);const r=t("<pre>").addClass("preload-editor-config-review").text(this.getConfigJSON()),o=t("<div>").addClass("preload-editor-config-review-wrap").append(r);i.append(o);const s=t("<div>").addClass("preload-editor-prompt-buttons"),n=this.createButton("Close","preload-editor-button-primary",()=>{a.remove(),e()});s.append(n),i.append(s),a.append(i),t(document.body).append(a),a.attr("tabindex","-1"),a.trigger("focus"),a.on("keydown.preloadEditor",e=>{"Escape"===e.key&&n.trigger("click")}),a.on("click.preloadEditor",e=>{e.target===a.get(0)&&n.trigger("click")})})},
async confirmSave(e,t){const a=t.replace(/\/+$/,"")+"/config.json";let i;i="preload"===e?"Save the preload page only?":"config"===e?"Save the config.json page only?":"Save both the preload and config.json?";let r=[];"preload"!==e&&"both"!==e||r.push("Preload: "+t),"config"!==e&&"both"!==e||(r.push("Config: "+a),r.push("Config content model: JSON"));return this.createConfirmDialog("Confirm save",i+"\n\n"+r.join("\n")+"\n\nThis will write the changes to the wiki.","Save","Cancel")},
destroyEditor(){if(!this.editor)return this.editorView=null,void(this.editorTools=null);const{view:e,inputHandler:t}=this.editor;e&&t&&e.dom.removeEventListener("input",t),e&&e.destroy(),this.editor=null,this.editorView=null,this.editorTools=null},
getEditorView(){return this.editorView},
getEditorContent(){return this.editorView&&this.editorView.state?this.editorView.state.doc.toString():this.state.preloadContent||""},
setEditorContent(e){if(e=String(e||""),!this.editorView)return void(this.state.preloadContent=e);this.editorView.state.doc.toString()!==e&&this.editorView.dispatch({changes:{from:0,to:this.editorView.state.doc.length,insert:e}})},
getEditorSelection(){if(!this.editorView||!this.editorView.state)return null;const e=this.editorView.state.selection.main;return{from:e.from,to:e.to,text:this.editorView.state.doc.sliceString(e.from,e.to)}},
focusEditor(){this.editorView&&this.editorView.focus()},
insertEditorText(e,t,a,i){if(!this.editorView)return;const r=this.editorView,o=r.state.selection.main,s=void 0===t?o.from:t,n=void 0===a?o.to:a,d=void 0===i?s+e.length:i;r.dispatch({changes:{from:s,to:n,insert:e},selection:{anchor:d}}),r.focus()},
wrapSelectionWithVariable(){const e=this.getEditorSelection();if(!e||e.from===e.to)return void this.focusEditor();const t="%%"+e.text+"%%";this.insertEditorText(t,e.from,e.to,e.from+t.length)},
insertVariable(e){if(!(e=String(e||"").trim().toUpperCase()))return;const t=this.getEditorSelection();t&&this.insertEditorText("%%"+e+"%%",t.from,t.to)},
insertCondition(e,t){e=String(e||"").toLowerCase(),t=String(t||"").trim().toUpperCase();const a=this.getEditorSelection();if(!a)return;let i,r;switch(e){case"if":{if(!t)return;const e="<!-- @if "+t+" -->",o="<!-- @endif -->";a.from!==a.to?(i=e+"\n"+a.text+"\n"+o,r=i.length):(i=e+"\n\n"+o,r=e.length+1);break}case"elseif":i="<!-- @elseif -->",r=i.length;break;case"else":i="<!-- @else -->",r=i.length;break;case"endif":i="<!-- @endif -->",r=i.length;break;default:return}this.insertEditorText(i,a.from,a.to,a.from+r)},
getToolVariables(){const e=new Set(["PAGE"]);return this.analysis&&this.analysis.variables&&this.analysis.variables.forEach(t=>e.add(String(t).toUpperCase())),Array.from(e)},
createToolSelect(e,a){const i=t("<select>").addClass("preload-editor-tool-select");return a&&i.addClass(a),e.forEach(e=>{t("<option>").attr("value",e.value).text(e.label).appendTo(i)}),i},
updateEditorToolVariables(){if(!this.editorTools)return;const e=this.getToolVariables(),a=this.editorTools.variableSelect.val(),i=this.editorTools.conditionVariableSelect.val(),r=(a,i)=>{a.empty(),e.forEach(e=>t("<option>").attr("value",e).text(e).appendTo(a)),i&&e.includes(i)?a.val(i):e.length&&a.val(e[0])};r(this.editorTools.variableSelect,a),r(this.editorTools.conditionVariableSelect,i)},
updateConditionVariableVisibility(){if(!this.editorTools)return;const e=this.editorTools.conditionTypeSelect.val();this.editorTools.conditionVariableWrap.toggle("if"===e)},
renderEditorTools(){const e=t("<div>").addClass("preload-editor-tools"),a=t("<div>").addClass("preload-editor-tool-group preload-editor-tool-group-wrap");a.append(this.createButton("Wrap selection","preload-editor-tool-button",()=>this.wrapSelectionWithVariable()));const i=t("<div>").addClass("preload-editor-tool-group preload-editor-tool-group-variable");t("<span>").addClass("preload-editor-tool-label").text("Insert existing variable").appendTo(i);const r=this.createToolSelect([],"preload-editor-variable-select");i.append(r,this.createButton("Insert","preload-editor-tool-button",()=>this.insertVariable(r.val())));const o=t("<div>").addClass("preload-editor-tool-group preload-editor-tool-group-condition");t("<span>").addClass("preload-editor-tool-label").text("Insert condition").appendTo(o);const s=this.createToolSelect([{value:"if",label:"@if"},{value:"elseif",label:"@elseif"},{value:"else",label:"@else"},{value:"endif",label:"@endif"}],"preload-editor-condition-type-select"),n=this.createToolSelect([],"preload-editor-condition-variable-select"),d=t("<span>").addClass("preload-editor-condition-variable").append(n),l=this.createButton("Insert","preload-editor-tool-button",()=>this.insertCondition(s.val(),n.val()));return s.on("change.preloadEditor",()=>this.updateConditionVariableVisibility()),o.append(s,d,l),e.append(a,i,o),this.editorTools={root:e,variableSelect:r,conditionTypeSelect:s,conditionVariableSelect:n,conditionVariableWrap:d,insertCondition:l},this.updateEditorToolVariables(),this.updateConditionVariableVisibility(),e},
updateEditorTools(){this.updateEditorToolVariables(),this.updateConditionVariableVisibility()},
getKnownVariables(){const e=new Set(["PAGE"]),t=this.state.config&&this.state.config.fields;return t&&"object"==typeof t&&Object.keys(t).forEach(t=>e.add(String(t).toUpperCase())),e},
createSyntaxDecorations(e,t){const a=[],i=e.doc.toString(),r=this.getKnownVariables(),o=/%%([A-Za-z0-9_-]+)%%/g;let s;for(;null!==(s=o.exec(i));){const e=s[1].toUpperCase(),i=r.has(e);a.push(t.mark({class:["preload-editor-syntax-variable",i?"preload-editor-syntax-variable-known":"preload-editor-syntax-variable-unknown"].join(" ")}).range(s.index,s.index+s[0].length))}const n=/<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;for(;null!==(s=n.exec(i));){a.push(t.mark({class:"preload-editor-syntax-condition"}).range(s.index,s.index+s[0].length));const e=s.index+s[0].indexOf("@");a.push(t.mark({class:"preload-editor-syntax-directive"}).range(e,e+s[1].length+1));const i=s[1].toLowerCase();if("if"!==i&&"elseif"!==i)continue;const o=s[2],n=s.index+s[0].indexOf(o),d=/[A-Za-z0-9_-]+/g;let l;for(;null!==(l=d.exec(o));){const e=l[0].toUpperCase();if(!r.has(e))continue;const i=n+l.index;a.push(t.mark({class:"preload-editor-syntax-condition-variable"}).range(i,i+l[0].length))}}return t.set(a,!0)},
async installSyntaxHighlighting(t){await e.loader.using("ext.CodeMirror.v6.lib");const a=e.loader.require,{StateField:i,StateEffect:r,EditorView:o,Decoration:s}=a("ext.CodeMirror.v6.lib"),n=i.define({create:e=>this.createSyntaxDecorations(e,s),update:(e,t)=>t.docChanged?this.createSyntaxDecorations(t.state,s):e,provide:e=>o.decorations.from(e)});t.dispatch({effects:r.appendConfig.of(n)}),this.editor.syntaxField=n},
async mountCodeMirror(t){return new Promise((a,i)=>{let r=!1;const o=()=>{e.hook("dev.CCM.ready").remove(s),e.hook("dev.CCM.load").remove(n)},s=async e=>{if(r||!e||e.textarea!==t||!e.view)return;r=!0,o();const s=e.view,n=()=>{this.state.preloadContent=s.state.doc.toString(),this.markPreloadDirty(),this.reanalyze(),this.renderVariablesBySelector(),this.renderVariableConfigurationBySelector(),this.renderAnalysisBySelector(),this.updateEditorTools(),this.saveDraft()};s.dom.addEventListener("input",n),s.dom.classList.add("preload-editor-codemirror");const d=s.dom.closest(".ext-codemirror-wrapper");d&&d.classList.add("preload-editor-codemirror-wrapper"),this.editor={cm:e,view:s,textarea:t,inputHandler:n},this.editorView=s,this.state.preloadContent=s.state.doc.toString();try{await this.installSyntaxHighlighting(s),this.state.dirty&&this.saveDraft(),a(s)}catch(e){i(e)}},n=e=>{if(!r&&"function"==typeof e)try{e(t)}catch(e){r=!0,o(),i(e)}};e.hook("dev.CCM.ready").add(s),e.hook("dev.CCM.load").add(n)})},
init(){const e=t("#mw-content-text");e.length&&(this.root=t("<div>").attr("id","preload-editor").addClass("preload-editor"),e.append(this.root),this.renderShell(),this.restoreDraft(),window.addEventListener("beforeunload",()=>{this.state.dirty&&this.saveDraft()}))},
renderShell(){this.destroyEditor(),this.root.empty();const e=this.createSection("Preload"),a=this.textInput(this.state.preloadTitle,"Template:Example/Preload").attr("id","preload-editor-title-input");a.on("input.preloadEditor",()=>{this.state.preloadTitle=String(a.val()||"").trim(),this.state.dirty&&"new"===this.state.mode&&this.saveDraft()});const i=this.createButton("Load","preload-editor-button-primary",()=>this.loadExisting(a.val())),r=this.createButton("New","",()=>this.startNew()),o=this.createButton("Load Sample Page","",()=>this.loadSamplePage());e.append(t("<div>").addClass("preload-editor-toolbar").append(a,i,r,o)),this.root.append(e),this.workspace=t("<div>").addClass("preload-editor-workspace"),this.root.append(this.workspace);const s=t("<div>").addClass("preload-editor-footer");this.localDraftStatus=t("<div>").addClass("preload-editor-local-draft-status is-empty").text("No local draft"),this.status=t("<div>").addClass("preload-editor-status"),s.append(this.localDraftStatus,this.status),this.root.append(s),this.updateLocalDraftIndicator(),this.renderWelcome()},
renderWelcome(){this.workspace.empty();const e=this.createSection("Get started");t("<p>").text("Load an existing preload, create a new one, or start from a sample wiki page.").appendTo(e);const a=t("<ul>").addClass("preload-editor-tips");["Existing preload: loads the preload and its config.json if available.","New preload: starts with the standard Preload Template structure.","Sample page: loads normal wiki page content so you can turn it into a preload."].forEach(e=>t("<li>").text(e).appendTo(a)),e.append(a),this.workspace.append(e)},
setStatus(e,t){this.status&&(this.status.removeClass("is-error","is-success","is-warning","is-loading"),t&&this.status.addClass("is-"+t),this.status.text(e||""))},
async loadExisting(t){if(t=String(t||"").trim()){this.setStatus("Loading preload...","loading");try{const e=await this.getPage(t);if(!e){const e=await this.findSimilarPreloads(t);return this.renderMissingPage(t,e),void this.setStatus("Preload not found.","warning")}await this.loadDefinition(e.title,e.content),this.setStatus("Preload loaded.","success")}catch(e){console.error("Preload Editor:",e),this.setStatus(e.message||"Could not load preload.","error")}}else e.notify("Enter a preload page title first.",{type:"warn"})},
async findSimilarPreloads(e){let t=e.replace(/\/Preload$/i,"").trim();t||(t=e);try{return(await this.searchPages(t)).filter(e=>/\/Preload$/i.test(e.title))}catch(e){return console.error("Preload Editor search:",e),[]}},
renderMissingPage(e,a){this.workspace.empty();const i=this.createSection("Preload not found");if(t("<p>").text('No preload exists at "'+e+'".').appendTo(i),a.length){t("<h3>").text("Similar preloads").appendTo(i);const e=t("<div>").addClass("preload-editor-suggestions");a.forEach(t=>e.append(this.createButton(t.title,"preload-editor-suggestion",()=>this.loadExisting(t.title)))),i.append(e)}i.append(this.createButton("Create this preload","preload-editor-button-primary",()=>this.startNew(e))),this.workspace.append(i)},
async startNew(e){e=String(e||"").trim(),this.state={preloadTitle:e,preloadSource:"",preloadContent:this.defaults.preload,config:this.emptyConfig(),variables:["PAGE"],conditions:[],selectedVariable:"PAGE",deletedVariables:[],mode:"new",dirty:!0,dirtyPreload:!0,dirtyConfig:!0,localDraftSavedAt:null,localDraftAvailable:!1},this.saveDraft(),this.renderEditor(),this.setStatus("Creating a new preload.","success")},
async loadSamplePage(){const e=await this.createPromptDialog("Load Sample Page","Enter the title of a normal wiki page to use as the starting point.");if(e){this.setStatus("Loading sample page...","loading");try{const t=await this.getPage(e);if(!t)throw new Error("Sample page not found: "+e);const a=this.convertSampleToPreload(t.content);this.state={preloadTitle:"",preloadSource:"",preloadContent:a,config:this.emptyConfig(),variables:["PAGE"],conditions:[],selectedVariable:"PAGE",deletedVariables:[],mode:"new",dirty:!0,dirtyPreload:!0,dirtyConfig:!0,localDraftSavedAt:null,localDraftAvailable:!1},this.reanalyze(),this.saveDraft(),this.renderEditor(),this.setStatus("Sample page loaded.","success")}catch(e){console.error("Preload Editor sample:",e),this.setStatus(e.message||"Could not load sample page.","error")}}},
convertSampleToPreload(e){let t=String(e||"").trim();const a=t.match(/<includeonly>([\s\S]*?)<\/includeonly>/i);return a&&(t=a[1].trim()),t=t.replace(/<noinclude>[\s\S]*?<\/noinclude>/gi,"").trim(),"<includeonly>\n"+t+"\n</includeonly>\n<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n"},
async loadDefinition(e,t){const a=this.extractIncludeonly(t);let i=this.emptyConfig();const r=e.replace(/\/+$/,"")+"/config.json",o=await this.getPage(r);if(o)try{i=JSON.parse(o.content)}catch(e){throw new Error("The existing config.json contains invalid JSON: "+e.message)}const s=this.analyzePreload(a);this.state.deletedVariables=[],i=this.prepareConfigForVariables(i,s),this.state={preloadTitle:e,preloadSource:t,preloadContent:t,config:i,variables:s.variables,conditions:s.conditions,selectedVariable:s.variables.includes("PAGE")?"PAGE":s.variables[0]||"PAGE",deletedVariables:[],mode:"edit",dirty:!1,dirtyPreload:!1,dirtyConfig:!1,localDraftSavedAt:null,localDraftAvailable:!1},this.clearDraft(e),this.renderEditor()},
renderEditor(){this.destroyEditor(),this.workspace.empty();const e=t("<div>").addClass("preload-editor-layout"),a=t("<div>").addClass("preload-editor-left"),i=t("<div>").addClass("preload-editor-right"),r=this.createSection("Preload","preload-editor-main-section"),o=t("<div>").addClass("preload-editor-meta");t("<strong>").text(this.state.preloadTitle||"New preload").appendTo(o),"new"===this.state.mode&&t("<span>").addClass("preload-editor-badge").text("NEW").appendTo(o),r.append(o),r.append(this.renderEditorTools());const s=this.textarea(this.state.preloadContent).attr("id","preload-editor-content").addClass("preload-editor-content"),n=t("<div>").addClass("preload-editor-codemirror-mount").append(s);r.append(n),a.append(r);const d=this.createSection("Analysis"),l=t("<div>").addClass("preload-editor-analysis");d.append(l);const c=this.createSection("Variables"),p=this.createButton("Detect variables","preload-editor-button-primary preload-editor-detect-variables",()=>this.detectVariables()),h=t("<div>").addClass("preload-editor-help preload-editor-detect-variables-help").text("Refresh the variable list after editing the preload.");c.append(t("<div>").addClass("preload-editor-variable-detect").append(p,h));const f=t("<div>").addClass("preload-editor-variables");c.append(f);const u=this.createSection("Variable configuration"),g=this.createButton("Import definitions","preload-editor-button-primary preload-editor-import-definitions",()=>this.importVariableDefinitions()),y=t("<div>").addClass("preload-editor-help preload-editor-import-definitions-help").text("Import variable definitions from another preload config.json. Existing definitions can be replaced."),v=t("<div>").addClass("preload-editor-import-definition-controls").append(g,y);u.append(v);const m=t("<div>").addClass("preload-editor-variable-configuration");u.append(m);const b=this.createSection("Configuration"),C=t("<div>").addClass("preload-editor-help").text("Review the exact config.json that will be uploaded to the wiki."),w=this.createButton("Review config.json","",()=>this.createConfigReviewDialog());b.append(C,w),i.append(d,c,u,b),e.append(a,i);const S=t("<div>").addClass("preload-editor-save-bar");S.append(this.createButton("Save Preload","",()=>this.savePreloadOnly()),this.createButton("Save Config","",()=>this.saveConfigOnly()),this.createButton("Save","preload-editor-button-primary",()=>this.saveAll())),this.root.find(".preload-editor-footer").append(S),this.workspace.append(e),this.reanalyze(),this.renderVariables(f),this.renderAnalysis(l),this.renderVariableConfiguration(m),this.updateEditorTools(),this.updateLocalDraftIndicator(),this.mountCodeMirror(s.get(0)).catch(e=>{console.error("Preload Editor CodeMirror:",e),this.setStatus(e.message||"Could not initialize CodeMirror.","error")})},
detectVariables(){const e=this.getEditorContent();this.state.preloadContent=e;const t=this.extractIncludeonly(e),a=this.analyzePreload(t);if(Array.isArray(this.state.deletedVariables)){const e=new Set(a.variables.map(e=>String(e).toUpperCase()));this.state.deletedVariables=this.state.deletedVariables.filter(t=>!e.has(String(t).toUpperCase()))}else this.state.deletedVariables=[];this.markDirty(),this.reanalyze(),this.renderVariablesBySelector(),this.renderVariableConfigurationBySelector(),this.renderAnalysisBySelector(),this.updateEditorTools(),this.saveDraft(),this.setStatus("Variables detected from the current preload.","success")},
reanalyze(){const e=this.getEditorContent(),t=this.extractIncludeonly(e),a=this.analyzePreload(t);this.state.preloadContent=e,this.state.variables=a.variables,this.state.conditions=a.conditions,this.state.config=this.prepareConfigForVariables(this.state.config||this.emptyConfig(),a),this.analysis=a;const i=["PAGE"].concat(a.variables.filter(e=>"PAGE"!==e));i.includes(this.state.selectedVariable)||(this.state.selectedVariable=i[0]||"PAGE")},
renderAnalysis(e){e.empty();const a=this.analysis||this.analyzePreload(this.extractIncludeonly(this.getEditorContent())),i=a.variables.length,r=a.conditions.length,o=Object.keys(this.state.config&&this.state.config.fields?this.state.config.fields:{}).length,s=t("<div>").addClass("preload-editor-analysis-summary"),n=(e,a)=>t("<div>").addClass("preload-editor-analysis-metric").append(t("<strong>").addClass("preload-editor-analysis-value").text(e),t("<span>").addClass("preload-editor-analysis-label").text(a));s.append(n(i,1===i?"variable":"variables"),n(r,1===r?"condition marker":"condition markers"),n(o,1===o?"configured field":"configured fields")),e.append(s),a.conditionVariables.length&&e.append(t("<div>").addClass("preload-editor-analysis-note").text("Condition variables: "+a.conditionVariables.join(", ")));const d=this.validateConfig(this.state.config,a);if(d.errors.length){const a=t("<ul>").addClass("preload-editor-validation-errors");d.errors.forEach(e=>t("<li>").text(e).appendTo(a)),e.append(a)}if(d.warnings.length){const a=t("<ul>").addClass("preload-editor-validation-warnings");d.warnings.forEach(e=>t("<li>").text(e).appendTo(a)),e.append(a)}d.errors.length||d.warnings.length||e.append(t("<div>").addClass("preload-editor-validation-success").text("Preload and configuration are in sync."))},
renderAnalysisBySelector(){const e=this.root&&this.root.find(".preload-editor-analysis");e&&e.length&&this.renderAnalysis(e)},
renderVariables(e){e.empty();const a=this.analysis||this.analyzePreload(this.extractIncludeonly(this.getEditorContent())),i=a.variables.slice(),r=["PAGE"];i.forEach(e=>{"PAGE"===e||r.includes(e)||r.push(e)}),(this.state.config&&this.state.config.fields?Object.keys(this.state.config.fields):[]).forEach(e=>{const t=e.toUpperCase();"PAGE"===t||r.includes(t)||r.push(t)}),(Array.isArray(this.state.deletedVariables)?this.state.deletedVariables:[]).forEach(e=>{const t=String(e).toUpperCase();"PAGE"===t||r.includes(t)||r.push(t)});const o=t("<div>").addClass("preload-editor-variable-list");r.forEach(e=>{const i=e===this.state.selectedVariable,r=a.variables.includes(e),s=!!(this.state.config&&this.state.config.fields&&this.state.config.fields[e]),n=t("<button>").attr({type:"button","data-variable":e}).addClass("preload-editor-variable-item").toggleClass("is-selected",i).on("click.preloadEditor",()=>this.selectVariable(e));t("<span>").addClass("preload-editor-variable-name").text(e).appendTo(n),"PAGE"===e?n.append(t("<span>").addClass("preload-editor-badge").text("AUTOMATIC")):s?r||n.append(t("<span>").addClass("preload-editor-badge is-warning").text("UNUSED")):n.append(t("<span>").addClass("preload-editor-badge is-warning").text(r?"NOT CONFIGURED":"DELETED")),o.append(n)}),e.append(o),r.length||e.append(t("<div>").addClass("preload-editor-empty").text("No variables found in the preload."))},
renderVariablesBySelector(){const e=this.root&&this.root.find(".preload-editor-variables");e&&e.length&&this.renderVariables(e)},
selectVariable(e){if(!(e=String(e||"").trim().toUpperCase()))return;const t=["PAGE"].concat((this.analysis&&this.analysis.variables?this.analysis.variables:[]).filter(e=>"PAGE"!==e));(this.state.config&&this.state.config.fields?Object.keys(this.state.config.fields).map(e=>e.toUpperCase()):[]).forEach(e=>{t.includes(e)||"PAGE"===e||t.push(e)}),t.includes(e)&&(this.state.selectedVariable=e,this.renderVariablesBySelector(),this.renderVariableConfigurationBySelector())},
renderVariableConfigurationBySelector(){const e=this.root&&this.root.find(".preload-editor-variable-configuration");e&&e.length&&this.renderVariableConfiguration(e)},
renderVariableConfiguration(e){e.empty();const a=this.analysis||this.analyzePreload(this.extractIncludeonly(this.getEditorContent())),i=this.state.selectedVariable||"PAGE",r=(this.state.config&&this.state.config.fields?this.state.config.fields:{})[i];if("PAGE"!==i){if(!r){const a=t("<div>").addClass("preload-editor-variable preload-editor-variable-missing");return t("<div>").addClass("preload-editor-variable-header").append(t("<h3>").text(i),t("<span>").addClass("preload-editor-badge is-warning").text("NOT CONFIGURED")).appendTo(a),t("<p>").addClass("preload-editor-analysis-note").text("This variable is currently not present in config.json.").appendTo(a),void e.append(a)}e.append(this.createVariableCard(i,r,a))}else e.append(this.createAutomaticPageCard(r||this.getAutomaticPageField()))},
createAutomaticPageCard(e){const a=t("<div>").addClass("preload-editor-variable preload-editor-variable-automatic").attr("data-variable","PAGE"),i=t("<div>").addClass("preload-editor-variable-header");i.append(t("<h3>").text("PAGE"),t("<span>").addClass("preload-editor-badge").text("AUTOMATIC")),a.append(i),t("<p>").addClass("preload-editor-analysis-note").text("PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.").appendTo(a);const r=this.textInput(e.label||"Page Name").prop("disabled",!0),o=this.textarea(e.description||"Name of the page.").addClass("preload-editor-small-textarea").prop("disabled",!0),s=this.textInput(e.type||"page").prop("disabled",!0),n=this.checkbox(!1!==e.required).prop("disabled",!0),d=this.textInput(e.namespace||"Main").prop("disabled",!0),l=this.checkbox(!0===e["allow-new"]).prop("disabled",!0);return a.append(this.fieldRow("Label",r),this.fieldRow("Description",o),this.fieldRow("Type",s),this.fieldRow("Required",n),this.fieldRow("Namespace",d),this.fieldRow("Allow new page",l)),a},
createVariableCard(e,a,i){const r=t("<div>").addClass("preload-editor-variable").attr("data-variable",e),o=t("<div>").addClass("preload-editor-variable-header");o.append(t("<h3>").text(e)),i.variables.includes(e)||o.append(t("<span>").addClass("preload-editor-badge is-warning").text("UNUSED")),r.append(o);const s=this.textInput(a.label||"");s.on("input.preloadEditor",()=>{a.label=s.val(),this.markConfigDirty(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Label",s));const n=this.textarea(a.description||"").addClass("preload-editor-small-textarea");n.on("input.preloadEditor",()=>{a.description=n.val(),this.markConfigDirty(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Description",n));const d=this.select([{value:"text",label:"Text"},{value:"textarea",label:"Textarea"},{value:"number",label:"Number"},{value:"boolean",label:"Boolean"},{value:"select",label:"Select"},{value:"multiselect",label:"Multi-select"},{value:"page",label:"Page"},{value:"user",label:"User"},{value:"date",label:"Date"},{value:"file",label:"File"}],a.type||"text");d.on("change.preloadEditor",()=>{a.type=d.val(),this.markConfigDirty(),this.renderVariableConfigurationBySelector(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Type",d));const l=this.checkbox(!0===a.required);l.on("change.preloadEditor",()=>{a.required=l.prop("checked"),this.markConfigDirty(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Required",l));const c=this.textInput(a.placeholder||"");c.on("input.preloadEditor",()=>{const e=c.val();e?a.placeholder=e:delete a.placeholder,this.markConfigDirty(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Placeholder",c));const p=this.textInput(void 0===a.default?"":a.default);if(p.on("input.preloadEditor",()=>{const e=p.val();e?a.default=e:delete a.default,this.markConfigDirty(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Default",p,"For text-like fields this is used as a placeholder. For select fields it is the empty/default option.")),"textarea"===a.type){const e=this.textInput(a.maxRows||"").attr("type","number");e.on("input.preloadEditor",()=>{const t=parseInt(e.val(),10);Number.isFinite(t)&&t>0?a.maxRows=t:delete a.maxRows,this.markConfigDirty(),this.renderConfigReviewAvailability()}),r.append(this.fieldRow("Maximum rows",e))}"select"!==a.type&&"multiselect"!==a.type||this.renderSelectConfiguration(r,a),"file"===a.type&&this.renderFileConfiguration(r,a);const h=this.createButton("Delete variable","preload-editor-button-danger preload-editor-delete-variable",async()=>{await this.createConfirmDialog("Delete variable",'Delete "'+e+'" from the configuration?\n\nThe variable will remain in the preload, but its config.json field will be removed.',"Delete variable","Cancel")&&this.deleteVariable(e)});return r.append(t("<div>").addClass("preload-editor-variable-delete").append(h)),r},
renderFileConfiguration(e,a){const i=t("<div>").addClass("preload-editor-file-configuration"),r=t("<h4>").text("File upload settings");i.append(r);const o=this.textInput(a.filename||"%%PAGE%%.%%EXT%%"),s=this.textarea(a.filetext||"").addClass("preload-editor-small-textarea"),n=this.textarea(Array.isArray(a.accept)?a.accept.join("\n"):"").addClass("preload-editor-small-textarea"),d=this.textInput(a.comment||"");o.on("input.preloadEditor",()=>{const e=o.val();e?a.filename=e:delete a.filename,this.markConfigDirty(),this.renderConfigReviewAvailability()}),s.on("input.preloadEditor",()=>{const e=s.val();e?a.filetext=e:delete a.filetext,this.markConfigDirty(),this.renderConfigReviewAvailability()}),n.on("input.preloadEditor",()=>{const e=n.val().split("\n").map(e=>e.trim()).filter(Boolean);e.length?a.accept=e:delete a.accept,this.markConfigDirty(),this.renderConfigReviewAvailability()}),d.on("input.preloadEditor",()=>{const e=d.val();e?a.comment=e:delete a.comment,this.markConfigDirty(),this.renderConfigReviewAvailability()}),i.append(this.fieldRow("Filename template",o,"The generated filename. Supports normal variables plus %%EXT%%, %%ORIGINAL%% and %%ORIGINAL_NAME%%.")),i.append(this.fieldRow("File page text",s,"Initial wikitext for the File: page. Normal %%VARIABLE%% substitutions are supported.")),i.append(this.fieldRow("Accepted MIME types",n,"One MIME type per line, for example image/png and image/gif.")),i.append(this.fieldRow("Upload summary",d,"Optional edit summary for the file upload.")),e.append(i)},
async deleteVariable(e){(e=String(e||"").trim().toUpperCase())&&"PAGE"!==e&&this.state.config&&this.state.config.fields&&(delete this.state.config.fields[e],Array.isArray(this.state.deletedVariables)||(this.state.deletedVariables=[]),this.state.deletedVariables.includes(e)||this.state.deletedVariables.push(e),this.state.selectedVariable=e,this.markConfigDirty(),this.renderVariablesBySelector(),this.renderVariableConfigurationBySelector(),this.renderAnalysisBySelector(),this.renderConfigReviewAvailability(),this.setStatus('Variable "'+e+'" removed from the configuration.',"warning"))},
renderSelectConfiguration(e,t){const a=this.textInput(t.source||"","category:Example");a.on("input.preloadEditor",()=>{const e=a.val();e?t.source=e:delete t.source,this.markConfigDirty(),this.renderConfigReviewAvailability()}),e.append(this.fieldRow("Source",a,"Currently supported source format: category:Category Name"));const i=this.textInput(t["source-prefix"]||"");i.on("input.preloadEditor",()=>{const e=i.val();e?t["source-prefix"]=e:delete t["source-prefix"],this.markConfigDirty(),this.renderConfigReviewAvailability()}),e.append(this.fieldRow("Source prefix",i,"Only source values beginning with this text are included."));const r=this.textInput(t.exclude&&t.exclude.category||"");r.on("input.preloadEditor",()=>{const e=r.val();e?t.exclude=Object.assign({},t.exclude||{},{category:e}):t.exclude&&(delete t.exclude.category,Object.keys(t.exclude).length||delete t.exclude),this.markConfigDirty(),this.renderConfigReviewAvailability()}),e.append(this.fieldRow("Exclude category",r)),e.append(this.createTransformControls(t,"value","Value transformation"),this.createTransformControls(t,"label","Label transformation"));const o=Array.isArray(t.options)?t.options.map(e=>"string"==typeof e?e:String(e.value||"")+" | "+String(e.label||e.value||"")).join("\n"):"",s=this.textarea(o).addClass("preload-editor-small-textarea");s.on("input.preloadEditor",()=>{const e=s.val().split("\n").map(e=>e.trim()).filter(Boolean);e.length?t.options=e.map(e=>{const t=e.split(/\s*\|\s*/);return 1===t.length?t[0]:{value:t[0],label:t.slice(1).join(" | ")}}):delete t.options,this.markConfigDirty(),this.renderConfigReviewAvailability()}),e.append(this.fieldRow("Manual options",s,"One per line. Optional label format: value | label. Leave empty when using a source."))},
createTransformControls(e,a,i){const r=t("<div>").addClass("preload-editor-transform");t("<h4>").text(i).appendTo(r);const o=e[a]&&"object"==typeof e[a]?e[a]:{},s=this.select([{value:"",label:"No transformation"},{value:"remove-prefix",label:"Remove prefix"},{value:"remove-suffix",label:"Remove suffix"},{value:"replace",label:"Replace"}],o.transform||""),n=this.textInput(o.prefix||""),d=this.textInput(o.suffix||""),l=this.textInput(o.from||""),c=this.textInput(o.to||""),p=t("<div>").addClass("preload-editor-transform-controls"),h=this.fieldRow("Prefix",n),f=this.fieldRow("Suffix",d),u=this.fieldRow("From",l),g=this.fieldRow("To",c),y=()=>{const e=s.val();h.toggle("remove-prefix"===e),f.toggle("remove-suffix"===e),u.toggle("replace"===e),g.toggle("replace"===e)},v=()=>{const t=s.val();let i=null;"remove-prefix"===t&&n.val()?i={transform:t,prefix:n.val()}:"remove-suffix"===t&&d.val()?i={transform:t,suffix:d.val()}:"replace"===t&&l.val()&&(i={transform:t,from:l.val(),to:c.val()}),i?e[a]=i:delete e[a],this.markConfigDirty(),this.renderConfigReviewAvailability()};return s.on("change.preloadEditor",()=>{v(),y()}),[n,d,l,c].forEach(e=>e.on("input.preloadEditor",v)),p.append(this.fieldRow("Transformation",s),h,f,u,g),y(),r.append(p),r},
getCleanConfig(){const e=JSON.parse(JSON.stringify(this.state.config)),t=e.fields||{},a={};return a.PAGE=this.getAutomaticPageField(),Object.keys(t).filter(e=>"PAGE"!==e.toUpperCase()).forEach(e=>{a[e]=t[e]}),e.fields=a,e},
getConfigJSON(){return JSON.stringify(this.getCleanConfig(),null,4)},
renderConfigPreview(e){e&&e.text(this.getConfigJSON())},
renderConfigPreviewBySelector(){this.renderAnalysisBySelector()},
renderConfigReviewAvailability(){this.renderAnalysisBySelector()},
updateDirtyState(){this.state.dirty=!(!this.state.dirtyPreload&&!this.state.dirtyConfig)},
markPreloadDirty(){this.state.dirtyPreload=!0,this.updateDirtyState(),this.saveDraft()},
markConfigDirty(){this.state.dirtyConfig=!0,this.updateDirtyState(),this.saveDraft()},
markDirty(){this.state.dirtyPreload=!0,this.state.dirtyConfig=!0,this.updateDirtyState(),this.saveDraft()},
async savePreloadOnly(){let t=String(this.state.preloadTitle||"").trim();if(!t){const e=await this.createPromptDialog("Preload title","Enter the full title for the preload.");if(!e)return;t=e,this.state.preloadTitle=t}this.state.preloadContent=this.getEditorContent();const a=this.validateBeforeSave();if(a.errors.length)return this.showValidationErrors(a),void this.saveDraft();if(await this.confirmSave("preload",t)){this.setStatus("Saving preload...","loading");try{const a=await this.pageExists(t);await this.savePage(t,this.state.preloadContent,a?"Updated preload with Preload Editor":"Created preload with Preload Editor",!a),this.state.mode="edit",this.state.preloadTitle=t,this.state.dirtyPreload=!1,this.updateDirtyState(),this.state.dirty?this.saveDraft():this.clearDraft(t),this.setStatus(this.state.dirtyConfig?"Preload saved. Config still has unsaved changes.":"Preload saved successfully.","success"),e.notify(this.state.dirtyConfig?"Preload saved. Config still has unsaved changes.":"Preload saved.",{type:"success"})}catch(t){console.error("Preload Editor save:",t),this.saveDraft(),this.setStatus(t.message||"Could not save preload.","error"),e.notify(t.message||"Could not save preload.","error")}}},
async saveConfigOnly(){let t=String(this.state.preloadTitle||"").trim();if(!t){const e=await this.createPromptDialog("Preload title","Enter the full title for the preload.");if(!e)return;t=e,this.state.preloadTitle=t}this.state.preloadContent=this.getEditorContent(),this.reanalyze();const a=this.validateBeforeSave();if(a.errors.length)return this.showValidationErrors(a),void this.saveDraft();if(!await this.confirmSave("config",t))return;const i=t.replace(/\/+$/,"")+"/config.json";this.setStatus("Saving config...","loading");try{await this.saveConfigPage(i,this.getConfigJSON(),await this.pageExists(i)?"Updated preload config with Preload Editor":"Created preload config with Preload Editor"),this.state.mode="edit",this.state.preloadTitle=t,this.state.dirtyConfig=!1,this.updateDirtyState(),this.state.dirty?this.saveDraft():this.clearDraft(t),this.setStatus(this.state.dirtyPreload?"Config saved as JSON. Preload still has unsaved changes.":"Config saved as JSON successfully.","success"),e.notify(this.state.dirtyPreload?"Config saved as JSON. Preload still has unsaved changes.":"Config saved as JSON.",{type:"success"})}catch(t){console.error("Preload Editor config save:",t),this.saveDraft(),this.setStatus(t.message||"Could not save config.","error"),e.notify(t.message||"Could not save config.","error")}},
async saveAll(){let t=String(this.state.preloadTitle||"").trim();if(!t){const e=await this.createPromptDialog("Preload title","Enter the full title for the preload.");if(!e)return;t=e,this.state.preloadTitle=t}this.state.preloadContent=this.getEditorContent(),this.reanalyze();const a=this.validateBeforeSave();if(a.errors.length)return this.showValidationErrors(a),void this.saveDraft();if(!await this.confirmSave("both",t))return;const i=t.replace(/\/+$/,"")+"/config.json";this.setStatus("Saving preload and config...","loading");try{const a=await this.pageExists(t);await this.savePage(t,this.state.preloadContent,a?"Updated preload with Preload Editor":"Created preload with Preload Editor",!a);const r=await this.pageExists(i);await this.saveConfigPage(i,this.getConfigJSON(),r?"Updated preload config with Preload Editor":"Created preload config with Preload Editor"),this.state.mode="edit",this.state.dirtyPreload=!1,this.state.dirtyConfig=!1,this.state.dirty=!1,this.state.preloadTitle=t,this.clearDraft(t),this.setStatus("Preload and JSON config saved successfully.","success"),e.notify("Preload and config saved. config.json is using the JSON content model.",{type:"success"})}catch(t){console.error("Preload Editor save:",t),this.saveDraft(),this.setStatus(t.message||"Could not save preload/config.","error"),e.notify(t.message||"Could not save preload/config.","error")}},
validateBeforeSave(){const e=[],t=[],a=String(this.state.preloadTitle||"").trim();a||e.push("A preload title is required."),a&&!/\/Preload$/i.test(a)&&t.push('The title does not end with "/Preload".');const i=this.getEditorContent();this.state.preloadContent=i,i.trim()||e.push("The preload cannot be empty."),/<includeonly>/i.test(i)||t.push("The preload does not contain an <includeonly> section."),/<noinclude>/i.test(i)||t.push("The preload does not contain a <noinclude> section.");const r=this.analyzePreload(this.extractIncludeonly(i));this.state.config=this.prepareConfigForVariables(this.state.config||this.emptyConfig(),r);const o=this.validateConfig(this.state.config,r);e.push(...o.errors),t.push(...o.warnings);let s=0;return r.conditions.forEach(t=>{"if"===t.type?s++:"endif"===t.type?(s--,s<0&&(e.push("Found @endif without a matching @if."),s=0)):"elseif"!==t.type&&"else"!==t.type||0===s&&e.push("@"+t.type+" exists outside an @if block.")}),s>0&&e.push("One or more @if blocks are missing @endif."),{errors:e,warnings:t,valid:0===e.length}},
showValidationErrors(t){this.renderAnalysisBySelector(),this.saveDraft(),this.setStatus(t.errors.join(" "),"error"),e.notify(t.errors.join("\n"),{type:"error"})}};e.PreloadEditor=a,t(function(){"Petit_Planet:Preload_Editor"===e.config.get("wgPageName")&&a.init()})}(mediaWiki,jQuery);