|
|
| Line 1: |
Line 1: |
| (function (mw, $) {
| | !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"}, |
| 'use strict';
| |
|
| |
|
| const PreloadEditor = {
| | 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:{}}}}, |
| version: '1.1.0',
| |
|
| |
|
| state: {
| | 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}}, |
| preloadTitle: '',
| |
| preloadSource: '',
| |
| preloadContent: '',
| |
| config: null,
| |
| variables: [],
| |
| conditions: [],
| |
| selectedVariable: 'PAGE',
| |
| deletedVariables: [],
| |
| mode: 'idle',
| |
| dirty: false,
| |
| dirtyPreload: false,
| |
| dirtyConfig: false,
| |
|
| |
|
| // Local draft state
| | getDraftId:e=>(e=String(e||"").trim())?e.toLowerCase():"__untitled__", |
| localDraftSavedAt: null,
| |
| localDraftAvailable: false
| |
| },
| |
|
| |
|
| editor: null,
| | 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()}}, |
| editorView: null,
| |
| analysis: null,
| |
| root: null,
| |
| workspace: null,
| |
| status: null,
| |
| localDraftStatus: null,
| |
| editorTools: null,
| |
|
| |
|
| draftStorageKey:
| | 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||""))}, |
| 'PetitPlanet.PreloadEditor.drafts.v1',
| |
|
| |
|
| defaults: {
| | formatLocalDraftTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch(e){return""}}, |
| preload:
| |
| '<includeonly>\n<!-- preload here -->\n</includeonly>\n<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
| |
| },
| |
|
| |
|
| // =========================================================
| | 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")}}, |
| // Browser-local draft storage
| |
| // =========================================================
| |
|
| |
|
| getDraftStorage() {
| | 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}, |
| try {
| |
| const raw =
| |
| window.localStorage.getItem(
| |
| this.draftStorageKey
| |
| );
| |
|
| |
|
| if (!raw) {
| | 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()}, |
| return {
| |
| active: null,
| |
| drafts: {}
| |
| };
| |
| }
| |
|
| |
|
| const parsed =
| | 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}, |
| JSON.parse(raw);
| |
|
| |
|
| if (
| | 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}, |
| !parsed ||
| |
| typeof parsed !== 'object' ||
| |
| Array.isArray(parsed)
| |
| ) {
| |
| return {
| |
| active: null,
| |
| drafts: {}
| |
| };
| |
| }
| |
|
| |
|
| if (
| | 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"}}, |
| !parsed.drafts ||
| |
| typeof parsed.drafts !== 'object' ||
| |
| Array.isArray(parsed.drafts)
| |
| ) {
| |
| parsed.drafts = {};
| |
| }
| |
|
| |
|
| return parsed;
| | async pageExists(e){return!!await this.getPage(e)}, |
| } catch (error) {
| |
| console.warn(
| |
| 'Preload Editor: Could not read local drafts.',
| |
| error
| |
| );
| |
|
| |
|
| return {
| | 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||[]}, |
| active: null,
| |
| drafts: {}
| |
| };
| |
| }
| |
| },
| |
|
| |
|
| setDraftStorage(storage) {
| | 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)}, |
| try {
| |
| window.localStorage.setItem(
| |
| this.draftStorageKey,
| |
| JSON.stringify(storage)
| |
| );
| |
|
| |
|
| return true;
| | changeContentModel:async(t,a,i)=>(new e.Api).postWithToken("csrf",{action:"changecontentmodel",title:t,model:a,reason:i||"Changed content model with Preload Editor"}), |
| } catch (error) {
| |
| console.warn(
| |
| 'Preload Editor: Could not save local draft.',
| |
| error
| |
| );
| |
|
| |
|
| return false;
| | 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")}, |
| }
| |
| },
| |
|
| |
|
| getDraftId(title) {
| | normalizeConfigTitle:e=>(e=(e=String(e||"").trim()).replace(/\/+$/,""),/\/config\.json$/i.test(e)?e:e+"/config.json"), |
| title =
| |
| String(title || '').trim();
| |
|
| |
|
| return title
| | cloneConfigField(e){try{return JSON.parse(JSON.stringify(e))}catch(e){return console.warn("Preload Editor: Could not clone imported field.",e),null}}, |
| ? title.toLowerCase()
| |
| : '__untitled__';
| |
| },
| |
|
| |
|
| getDraftSnapshot() {
| | 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}, |
| let content =
| |
| this.getEditorContent();
| |
|
| |
|
| if (
| | 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")})})}, |
| !this.editorView &&
| |
| !content
| |
| ) {
| |
| content =
| |
| this.state.preloadContent ||
| |
| '';
| |
| }
| |
|
| |
|
| let config = null;
| | 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"})}}, |
|
| |
|
| try {
| | extractIncludeonly(e){const t=String(e||"").match(/<includeonly>([\s\S]*?)<\/includeonly>/i);return t?t[1].trim():String(e||"").trim()}, |
| config =
| |
| this.state.config
| |
| ? JSON.parse(
| |
| JSON.stringify(
| |
| this.state.config
| |
| )
| |
| )
| |
| : this.emptyConfig();
| |
| } catch (error) {
| |
| console.warn(
| |
| 'Preload Editor: Could not clone config for local draft.',
| |
| error
| |
| );
| |
|
| |
|
| config =
| | 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}, |
| this.emptyConfig();
| |
| }
| |
|
| |
|
| return {
| | 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}, |
| version: 3,
| |
|
| |
|
| preloadTitle:
| | 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}, |
| String(
| |
| this.state.preloadTitle || ''
| |
| ),
| |
|
| |
|
| preloadSource:
| | analyzePreload(e){return{variables:this.findVariables(e),conditions:this.findConditions(e),conditionVariables:this.findConditionVariables(e)}}, |
| String(
| |
| this.state.preloadSource || ''
| |
| ),
| |
|
| |
|
| preloadContent:
| | getAutomaticPageField:()=>({label:"Page Name",description:"Name of the page.",type:"page",required:!0,namespace:"Main","allow-new":!0}), |
| String(
| |
| content || ''
| |
| ),
| |
|
| |
|
| config,
| | emptyConfig(){return{settings:{incomplete:"add-stub"},fields:{PAGE:this.getAutomaticPageField()}}}, |
|
| |
|
| variables:
| | 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}, |
| Array.isArray(
| |
| this.state.variables
| |
| )
| |
| ? this.state.variables.slice()
| |
| : [],
| |
|
| |
|
| conditions:
| | createDefaultField(e,t){return{label:this.humanizeName(e),description:"",type:t.conditionVariables.includes(e)?"boolean":"text"}}, |
| Array.isArray(
| |
| this.state.conditions
| |
| )
| |
| ? this.state.conditions.slice()
| |
| : [],
| |
|
| |
|
| selectedVariable:
| | 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()), |
| String(
| |
| this.state.selectedVariable ||
| |
| 'PAGE'
| |
| ),
| |
|
| |
|
| deletedVariables:
| | 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}, |
| Array.isArray(
| |
| this.state.deletedVariables
| |
| )
| |
| ? this.state.deletedVariables.slice()
| |
| : [],
| |
|
| |
|
| mode:
| | 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}}, |
| this.state.mode ||
| |
| 'edit',
| |
|
| |
|
| dirty:
| | 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}}, |
| !!this.state.dirty,
| |
|
| |
|
| dirtyPreload:
| | 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}, |
| !!this.state.dirtyPreload,
| |
|
| |
|
| dirtyConfig:
| | 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}, |
| !!this.state.dirtyConfig,
| |
|
| |
|
| savedAt:
| | 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)}, |
| Date.now()
| |
| };
| |
| },
| |
|
| |
|
| updateLocalDraftStatus(
| | textInput:(e,a)=>t("<input>").attr({type:"text",value:null==e?"":e,placeholder:a||""}).addClass("preload-editor-input"), |
| message,
| |
| type
| |
| ) {
| |
| if (!this.localDraftStatus) {
| |
| return;
| |
| }
| |
|
| |
|
| this.localDraftStatus.removeClass(
| | textarea:e=>t("<textarea>").val(null==e?"":e).addClass("preload-editor-textarea"), |
| 'is-saved',
| |
| 'is-warning',
| |
| 'is-error',
| |
| 'is-empty'
| |
| );
| |
|
| |
|
| if (type) {
| | checkbox:e=>t("<input>").attr("type","checkbox").prop("checked",!!e).addClass("preload-editor-checkbox"), |
| this.localDraftStatus.addClass(
| |
| 'is-' + type
| |
| );
| |
| }
| |
|
| |
|
| this.localDraftStatus.text(
| | 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}, |
| message || ''
| |
| );
| |
| },
| |
|
| |
|
| formatLocalDraftTime(timestamp) {
| | 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")})})}, |
| if (!timestamp) {
| |
| return '';
| |
| }
| |
|
| |
|
| try {
| | 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")})})}, |
| return new Date(
| |
| timestamp
| |
| ).toLocaleTimeString(
| |
| [],
| |
| {
| |
| hour: '2-digit',
| |
| minute: '2-digit',
| |
| second: '2-digit'
| |
| }
| |
| );
| |
| } catch (error) {
| |
| return '';
| |
| }
| |
| },
| |
|
| |
|
| updateLocalDraftIndicator() {
| | 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")})})}, |
| if (!this.localDraftStatus) {
| |
| return;
| |
| }
| |
|
| |
|
| if (
| | 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")}, |
| this.state.localDraftSavedAt
| |
| ) {
| |
| const time =
| |
| this.formatLocalDraftTime(
| |
| this.state.localDraftSavedAt
| |
| );
| |
|
| |
|
| this.updateLocalDraftStatus(
| | 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}, |
| time
| |
| ? 'Saved locally at ' + time
| |
| : 'Saved locally',
| |
| 'saved'
| |
| );
| |
|
| |
|
| return;
| | getEditorView(){return this.editorView}, |
| }
| |
|
| |
|
| this.updateLocalDraftStatus(
| | getEditorContent(){return this.editorView&&this.editorView.state?this.editorView.state.doc.toString():this.state.preloadContent||""}, |
| 'No local draft',
| |
| 'empty'
| |
| );
| |
| },
| |
|
| |
|
| saveDraft() {
| | 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}})}, |
| if (!this.state.dirty) {
| |
| return false;
| |
| }
| |
|
| |
|
| const snapshot =
| | 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)}}, |
| this.getDraftSnapshot();
| |
|
| |
|
| const id =
| | focusEditor(){this.editorView&&this.editorView.focus()}, |
| this.getDraftId(
| |
| snapshot.preloadTitle
| |
| );
| |
|
| |
|
| const storage =
| | 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()}, |
| this.getDraftStorage();
| |
|
| |
|
| storage.drafts[id] =
| | 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)}, |
| snapshot;
| |
|
| |
|
| storage.active = id;
| | insertVariable(e){if(!(e=String(e||"").trim().toUpperCase()))return;const t=this.getEditorSelection();t&&this.insertEditorText("%%"+e+"%%",t.from,t.to)}, |
|
| |
|
| const saved =
| | 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)}, |
| this.setDraftStorage(
| |
| storage
| |
| );
| |
|
| |
|
| if (saved) {
| | 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)}, |
| this.state.localDraftSavedAt =
| |
| snapshot.savedAt;
| |
|
| |
|
| this.state.localDraftAvailable =
| | 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}, |
| true;
| |
|
| |
|
| this.updateLocalDraftIndicator();
| | 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)}, |
| } else {
| |
| this.state.localDraftAvailable =
| |
| false;
| |
|
| |
|
| this.updateLocalDraftStatus(
| | updateConditionVariableVisibility(){if(!this.editorTools)return;const e=this.editorTools.conditionTypeSelect.val();this.editorTools.conditionVariableWrap.toggle("if"===e)}, |
| 'Could not save local draft',
| |
| 'error'
| |
| );
| |
| }
| |
|
| |
|
| return saved;
| | 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}, |
| },
| |
|
| |
|
| clearDraft(title) {
| | updateEditorTools(){this.updateEditorToolVariables(),this.updateConditionVariableVisibility()}, |
| const storage =
| |
| this.getDraftStorage();
| |
|
| |
|
| const id =
| | 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}, |
| this.getDraftId(title);
| |
|
| |
|
| if (
| | 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)}, |
| storage.drafts &&
| |
| storage.drafts[id]
| |
| ) {
| |
| delete storage.drafts[id];
| |
| }
| |
|
| |
|
| if (
| | 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}, |
| storage.active === id
| |
| ) {
| |
| storage.active = null;
| |
| }
| |
|
| |
|
| this.setDraftStorage(
| | 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)})}, |
| storage
| |
| );
| |
|
| |
|
| this.state.localDraftSavedAt =
| | 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()}))}, |
| null;
| |
|
| |
|
| this.state.localDraftAvailable =
| | 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()}, |
| false;
| |
|
| |
|
| this.updateLocalDraftIndicator();
| | 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)}, |
| },
| |
|
| |
|
| getMostRecentDraft() {
| | 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||""))}, |
| const storage =
| |
| this.getDraftStorage();
| |
|
| |
|
| if (
| | 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"})}, |
| !storage.drafts ||
| |
| !Object.keys(
| |
| storage.drafts
| |
| ).length
| |
| ) {
| |
| return null;
| |
| }
| |
|
| |
|
| if (
| | 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),[]}}, |
| storage.active &&
| |
| storage.drafts[
| |
| storage.active
| |
| ]
| |
| ) {
| |
| return {
| |
| id: storage.active,
| |
| draft:
| |
| storage.drafts[
| |
| storage.active
| |
| ]
| |
| };
| |
| }
| |
|
| |
|
| let latestId = null;
| | 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)}, |
| let latestDraft = null;
| |
|
| |
|
| Object.entries(
| | 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")}, |
| storage.drafts
| |
| ).forEach(
| |
| ([id, draft]) => {
| |
| if (
| |
| !draft ||
| |
| typeof draft !== 'object'
| |
| ) {
| |
| return;
| |
| }
| |
|
| |
|
| if (
| | 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")}}}, |
| !latestDraft ||
| |
| (
| |
| Number(
| |
| draft.savedAt
| |
| ) || 0
| |
| ) >
| |
| (
| |
| Number(
| |
| latestDraft.savedAt
| |
| ) || 0
| |
| )
| |
| ) {
| |
| latestId = id;
| |
| latestDraft = draft;
| |
| }
| |
| }
| |
| );
| |
|
| |
|
| if (!latestDraft) {
| | 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"}, |
| return null;
| |
| }
| |
|
| |
|
| return {
| | 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()}, |
| id: latestId,
| |
| draft: latestDraft
| |
| };
| |
| },
| |
|
| |
|
| async restoreDraft() {
| | 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")})}, |
| const result =
| |
| this.getMostRecentDraft();
| |
|
| |
|
| if (
| | 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")}, |
| !result ||
| |
| !result.draft
| |
| ) {
| |
| return false;
| |
| }
| |
|
| |
|
| const draft =
| | 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")}, |
| result.draft;
| |
|
| |
|
| const title =
| | 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."))}, |
| String(
| |
| draft.preloadTitle || ''
| |
| ).trim();
| |
|
| |
|
| const displayTitle =
| | renderAnalysisBySelector(){const e=this.root&&this.root.find(".preload-editor-analysis");e&&e.length&&this.renderAnalysis(e)}, |
| title ||
| |
| 'an untitled preload';
| |
|
| |
|
| const savedAt =
| | 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."))}, |
| draft.savedAt
| |
| ? new Date(
| |
| draft.savedAt
| |
| ).toLocaleString()
| |
| : '';
| |
|
| |
|
| const message =
| | renderVariablesBySelector(){const e=this.root&&this.root.find(".preload-editor-variables");e&&e.length&&this.renderVariables(e)}, |
| 'Preload Editor found unsaved local work for ' +
| |
| displayTitle +
| |
| (
| |
| savedAt
| |
| ? '\n\nLast saved locally: ' +
| |
| savedAt
| |
| : ''
| |
| ) +
| |
| '\n\nRestore it?';
| |
|
| |
|
| const restore =
| | 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())}, |
| await this.createConfirmDialog(
| |
| 'Restore unsaved work',
| |
| message,
| |
| 'Restore',
| |
| 'Discard'
| |
| );
| |
|
| |
|
| if (!restore) {
| | renderVariableConfigurationBySelector(){const e=this.root&&this.root.find(".preload-editor-variable-configuration");e&&e.length&&this.renderVariableConfiguration(e)}, |
| this.clearDraft(title);
| |
| return false;
| |
| }
| |
|
| |
|
| let config =
| | 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()))}, |
| this.emptyConfig();
| |
|
| |
|
| if (
| | 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}, |
| draft.config &&
| |
| typeof draft.config === 'object' &&
| |
| !Array.isArray(draft.config)
| |
| ) {
| |
| try {
| |
| config =
| |
| this.normalizeConfig(
| |
| JSON.parse(
| |
| JSON.stringify(
| |
| draft.config
| |
| )
| |
| )
| |
| );
| |
| } catch (error) {
| |
| console.warn(
| |
| 'Preload Editor: Could not restore local config.',
| |
| error
| |
| );
| |
| }
| |
| }
| |
|
| |
|
| const hasDirtyFlags =
| | 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}, |
| typeof draft.dirtyPreload === 'boolean' ||
| |
| typeof draft.dirtyConfig === 'boolean';
| |
|
| |
|
| this.state = {
| | 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)}, |
| preloadTitle:
| |
| title,
| |
|
| |
|
| preloadSource:
| | 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"))}, |
| String(
| |
| draft.preloadSource || ''
| |
| ),
| |
|
| |
|
| preloadContent:
| | 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."))}, |
| String(
| |
| draft.preloadContent || ''
| |
| ),
| |
|
| |
|
| config,
| | 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}, |
|
| |
|
| variables:
| | 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}, |
| Array.isArray(
| |
| draft.variables
| |
| )
| |
| ? draft.variables.slice()
| |
| : [],
| |
|
| |
|
| conditions:
| | getConfigJSON(){return JSON.stringify(this.getCleanConfig(),null,4)}, |
| Array.isArray(
| |
| draft.conditions
| |
| )
| |
| ? draft.conditions.slice()
| |
| : [],
| |
|
| |
|
| selectedVariable:
| | renderConfigPreview(e){e&&e.text(this.getConfigJSON())}, |
| String(
| |
| draft.selectedVariable ||
| |
| 'PAGE'
| |
| ),
| |
|
| |
|
| deletedVariables:
| | renderConfigPreviewBySelector(){this.renderAnalysisBySelector()}, |
| Array.isArray(
| |
| draft.deletedVariables
| |
| )
| |
| ? draft.deletedVariables
| |
| .map(
| |
| name =>
| |
| String(
| |
| name
| |
| ).toUpperCase()
| |
| )
| |
| .filter(
| |
| name =>
| |
| name !==
| |
| 'PAGE'
| |
| )
| |
| : [],
| |
|
| |
|
| mode:
| | renderConfigReviewAvailability(){this.renderAnalysisBySelector()}, |
| draft.mode ||
| |
| 'edit',
| |
|
| |
|
| dirty:
| | updateDirtyState(){this.state.dirty=!(!this.state.dirtyPreload&&!this.state.dirtyConfig)}, |
| hasDirtyFlags
| |
| ? !!(
| |
| draft.dirtyPreload ||
| |
| draft.dirtyConfig
| |
| )
| |
| : true,
| |
|
| |
|
| dirtyPreload:
| | markPreloadDirty(){this.state.dirtyPreload=!0,this.updateDirtyState(),this.saveDraft()}, |
| hasDirtyFlags
| |
| ? !!draft.dirtyPreload
| |
| : true,
| |
|
| |
|
| dirtyConfig:
| | markConfigDirty(){this.state.dirtyConfig=!0,this.updateDirtyState(),this.saveDraft()}, |
| hasDirtyFlags
| |
| ? !!draft.dirtyConfig
| |
| : true,
| |
|
| |
|
| localDraftSavedAt:
| | markDirty(){this.state.dirtyPreload=!0,this.state.dirtyConfig=!0,this.updateDirtyState(),this.saveDraft()}, |
| Number(
| |
| draft.savedAt
| |
| ) || null,
| |
|
| |
|
| localDraftAvailable:
| | 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")}}}, |
| true
| |
| };
| |
|
| |
|
| this.reanalyze();
| | 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")}}, |
| this.renderEditor();
| | |
| | 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")}}, |
|
| |
|
| this.updateLocalDraftIndicator();
| | 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}}, |
|
| |
|
| this.setStatus(
| | 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); |
| 'Restored unsaved local work from this browser.',
| |
| 'warning'
| |
| );
| |
| | |
| mw.notify(
| |
| 'Unsaved Preload Editor work restored.',
| |
| {
| |
| type: 'success'
| |
| }
| |
| );
| |
| | |
| return true;
| |
| },
| |
| | |
| // =========================================================
| |
| // API
| |
| // =========================================================
| |
| | |
| async getPage(title) {
| |
| const response =
| |
| await new mw.Api().get({
| |
| action: 'query',
| |
| prop: 'info|revisions',
| |
| titles: title,
| |
| rvprop: 'content',
| |
| rvslots: 'main',
| |
| formatversion: 2
| |
| });
| |
| | |
| const page =
| |
| response &&
| |
| response.query &&
| |
| response.query.pages &&
| |
| response.query.pages[0];
| |
| | |
| if (!page || page.missing) {
| |
| return null;
| |
| }
| |
| | |
| const content =
| |
| page.revisions &&
| |
| page.revisions[0] &&
| |
| page.revisions[0].slots &&
| |
| page.revisions[0].slots.main &&
| |
| page.revisions[0].slots.main.content;
| |
| | |
| return {
| |
| title:
| |
| page.title ||
| |
| title,
| |
| | |
| content:
| |
| typeof content === 'string'
| |
| ? content
| |
| : '',
| |
| | |
| contentmodel:
| |
| page.contentmodel ||
| |
| 'wikitext'
| |
| };
| |
| },
| |
| | |
| async pageExists(title) {
| |
| return !!await this.getPage(title);
| |
| },
| |
| | |
| async searchPages(query) {
| |
| const response =
| |
| await new mw.Api().get({
| |
| action: 'query',
| |
| list: 'search',
| |
| srsearch: query,
| |
| srlimit: 10,
| |
| srnamespace: 10,
| |
| formatversion: 2
| |
| });
| |
| | |
| return (
| |
| response &&
| |
| response.query &&
| |
| response.query.search
| |
| ) || [];
| |
| },
| |
| | |
| /*
| |
| * Save a page.
| |
| *
| |
| * contentModel is mainly used when creating a page.
| |
| *
| |
| * For an existing page whose content model needs
| |
| * changing, use changeContentModel() first.
| |
| */
| |
| async savePage(
| |
| title,
| |
| content,
| |
| summary,
| |
| createOnly,
| |
| contentModel
| |
| ) {
| |
| const params = {
| |
| action: 'edit',
| |
| title,
| |
| text: content,
| |
| summary:
| |
| summary ||
| |
| 'Updated with Preload Editor',
| |
| formatversion: 2
| |
| };
| |
| | |
| if (createOnly) {
| |
| params.createonly = true;
| |
| }
| |
| | |
| if (contentModel) {
| |
| params.contentmodel =
| |
| contentModel;
| |
| }
| |
| | |
| return new mw.Api().postWithToken(
| |
| 'csrf',
| |
| params
| |
| );
| |
| },
| |
| | |
| /*
| |
| * Change the content model of an existing page.
| |
| *
| |
| * This is the API equivalent of using
| |
| * Special:ChangeContentModel manually.
| |
| *
| |
| * The page must already exist.
| |
| */
| |
| async changeContentModel(
| |
| title,
| |
| model,
| |
| summary
| |
| ) {
| |
| return new mw.Api().postWithToken(
| |
| 'csrf',
| |
| {
| |
| action:
| |
| 'changecontentmodel',
| |
| | |
| title,
| |
| | |
| model,
| |
| | |
| reason:
| |
| summary ||
| |
| 'Changed content model with Preload Editor'
| |
| }
| |
| );
| |
| },
| |
| | |
| /*
| |
| * Save config.json and make sure its content model
| |
| * is JSON.
| |
| *
| |
| * New page:
| |
| * create directly with contentmodel=json.
| |
| *
| |
| * Existing page:
| |
| * if not already JSON, change its model first,
| |
| * then save the JSON normally.
| |
| */
| |
| async saveConfigPage(
| |
| title,
| |
| content,
| |
| summary
| |
| ) {
| |
| const page =
| |
| await this.getPage(title);
| |
| | |
| if (!page) {
| |
| return this.savePage(
| |
| title,
| |
| content,
| |
| summary,
| |
| true,
| |
| 'json'
| |
| );
| |
| }
| |
| | |
| if (
| |
| String(
| |
| page.contentmodel || ''
| |
| ).toLowerCase() !== 'json'
| |
| ) {
| |
| await this.changeContentModel(
| |
| title,
| |
| 'json',
| |
| 'Changed preload config content model to JSON'
| |
| );
| |
| }
| |
| | |
| return this.savePage(
| |
| title,
| |
| content,
| |
| summary,
| |
| false
| |
| );
| |
| },
| |
| | |
| // =========================================================
| |
| // Config import helpers
| |
| // =========================================================
| |
| | |
| /*
| |
| * Accept either:
| |
| *
| |
| * Template:Example/Preload
| |
| *
| |
| * or:
| |
| *
| |
| * Template:Example/Preload/config.json
| |
| *
| |
| * and always return the config.json title.
| |
| */
| |
| normalizeConfigTitle(title) {
| |
| title =
| |
| String(title || '').trim();
| |
| | |
| title =
| |
| title.replace(
| |
| /\/+$/,
| |
| ''
| |
| );
| |
| | |
| if (
| |
| /\/config\.json$/i.test(
| |
| title
| |
| )
| |
| ) {
| |
| return title;
| |
| }
| |
| | |
| return title +
| |
| '/config.json';
| |
| },
| |
| | |
| /*
| |
| * Return a safe clone of a config field.
| |
| *
| |
| * This is deliberately JSON-based because config
| |
| * definitions are JSON-compatible objects.
| |
| */
| |
| cloneConfigField(field) {
| |
| try {
| |
| return JSON.parse(
| |
| JSON.stringify(field)
| |
| );
| |
| } catch (error) {
| |
| console.warn(
| |
| 'Preload Editor: Could not clone imported field.',
| |
| error
| |
| );
| |
| | |
| return null;
| |
| }
| |
| },
| |
| | |
| /*
| |
| * Normalize imported field names to the same uppercase
| |
| * convention used throughout the editor.
| |
| */
| |
| getImportableFields(sourceConfig) {
| |
| const fields =
| |
| sourceConfig &&
| |
| sourceConfig.fields;
| |
| | |
| if (
| |
| !fields ||
| |
| typeof fields !== 'object' ||
| |
| Array.isArray(fields)
| |
| ) {
| |
| return {};
| |
| }
| |
| | |
| const result = {};
| |
| | |
| Object.entries(
| |
| fields
| |
| ).forEach(
| |
| ([name, field]) => {
| |
| const upper =
| |
| String(
| |
| name || ''
| |
| )
| |
| .trim()
| |
| .toUpperCase();
| |
| | |
| if (
| |
| !upper ||
| |
| upper === 'PAGE'
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| if (
| |
| !field ||
| |
| typeof field !== 'object' ||
| |
| Array.isArray(field)
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| const clone =
| |
| this.cloneConfigField(
| |
| field
| |
| );
| |
| | |
| if (!clone) {
| |
| return;
| |
| }
| |
| | |
| result[upper] =
| |
| clone;
| |
| }
| |
| );
| |
| | |
| return result;
| |
| },
| |
| | |
| /*
| |
| * Show the variable-selection dialog after a source
| |
| * config has been loaded.
| |
| *
| |
| * Resolves:
| |
| * null -> cancelled
| |
| * [] -> confirmed with no selections
| |
| * [...] -> selected field names
| |
| */
| |
| createImportDefinitionsDialog(
| |
| sourceTitle,
| |
| fields
| |
| ) {
| |
| return new Promise(
| |
| resolve => {
| |
| const names =
| |
| Object.keys(
| |
| fields
| |
| ).sort(
| |
| (a, b) =>
| |
| a.localeCompare(
| |
| b
| |
| )
| |
| );
| |
| | |
| const overlay =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-overlay preload-editor-import-overlay'
| |
| );
| |
| | |
| const box =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt preload-editor-import-dialog'
| |
| );
| |
| | |
| $('<h2>')
| |
| .text(
| |
| 'Import variable definitions'
| |
| )
| |
| .appendTo(box);
| |
| | |
| $('<p>')
| |
| .text(
| |
| 'Choose which variable definitions to import from "' +
| |
| sourceTitle +
| |
| '". Existing local definitions can be replaced.'
| |
| )
| |
| .appendTo(box);
| |
| | |
| if (!names.length) {
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-empty preload-editor-import-empty'
| |
| )
| |
| .text(
| |
| 'This config.json does not contain any importable variable definitions.'
| |
| )
| |
| .appendTo(box);
| |
| | |
| const emptyButtons =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-buttons'
| |
| );
| |
| | |
| const close =
| |
| this.createButton(
| |
| 'Close',
| |
| 'preload-editor-button-primary',
| |
| () => {
| |
| overlay.remove();
| |
| resolve(null);
| |
| }
| |
| );
| |
| | |
| emptyButtons.append(
| |
| close
| |
| );
| |
| | |
| box.append(
| |
| emptyButtons
| |
| );
| |
| | |
| overlay.append(box);
| |
| | |
| $(document.body)
| |
| .append(overlay);
| |
| | |
| overlay.attr(
| |
| 'tabindex',
| |
| '-1'
| |
| );
| |
| | |
| overlay.trigger('focus');
| |
| | |
| return;
| |
| }
| |
| | |
| const search =
| |
| this.textInput(
| |
| '',
| |
| 'Search variables...'
| |
| )
| |
| .addClass(
| |
| 'preload-editor-import-search'
| |
| );
| |
| | |
| const searchWrap =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-import-search-wrap'
| |
| )
| |
| .append(search);
| |
| | |
| box.append(
| |
| searchWrap
| |
| );
| |
| | |
| const controls =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-import-controls'
| |
| );
| |
| | |
| const selectAll =
| |
| this.createButton(
| |
| 'Select all',
| |
| '',
| |
| () => {
| |
| list
| |
| .find(
| |
| 'input[type="checkbox"]:visible'
| |
| )
| |
| .prop(
| |
| 'checked',
| |
| true
| |
| );
| |
| | |
| updateSummary();
| |
| }
| |
| );
| |
| | |
| const deselectAll =
| |
| this.createButton(
| |
| 'Deselect all',
| |
| '',
| |
| () => {
| |
| list
| |
| .find(
| |
| 'input[type="checkbox"]:visible'
| |
| )
| |
| .prop(
| |
| 'checked',
| |
| false
| |
| );
| |
| | |
| updateSummary();
| |
| }
| |
| );
| |
| | |
| controls.append(
| |
| selectAll,
| |
| deselectAll
| |
| );
| |
| | |
| box.append(
| |
| controls
| |
| );
| |
| | |
| const list =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-import-list'
| |
| );
| |
| | |
| const localFields =
| |
| this.state.config &&
| |
| this.state.config.fields
| |
| ? this.state.config.fields
| |
| : {};
| |
| | |
| names.forEach(
| |
| name => {
| |
| const existing =
| |
| !!localFields[name];
| |
| | |
| const field =
| |
| fields[name];
| |
| | |
| const type =
| |
| field &&
| |
| field.type
| |
| ? String(
| |
| field.type
| |
| )
| |
| : 'text';
| |
| | |
| const label =
| |
| field &&
| |
| field.label
| |
| ? String(
| |
| field.label
| |
| )
| |
| : this.humanizeName(
| |
| name
| |
| );
| |
| | |
| const row =
| |
| $('<label>')
| |
| .addClass(
| |
| 'preload-editor-import-row'
| |
| )
| |
| .attr(
| |
| 'data-variable',
| |
| name
| |
| )
| |
| .attr(
| |
| 'data-search',
| |
| (
| |
| name +
| |
| ' ' +
| |
| label +
| |
| ' ' +
| |
| type
| |
| ).toLowerCase()
| |
| );
| |
| | |
| const checkbox =
| |
| this.checkbox(
| |
| false
| |
| )
| |
| .attr(
| |
| 'data-import-variable',
| |
| name
| |
| )
| |
| .addClass(
| |
| 'preload-editor-import-checkbox'
| |
| );
| |
| | |
| const content =
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-import-content'
| |
| );
| |
| | |
| const header =
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-import-header'
| |
| );
| |
| | |
| $('<strong>')
| |
| .text(name)
| |
| .appendTo(header);
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-import-type'
| |
| )
| |
| .text(type)
| |
| .appendTo(header);
| |
| | |
| const meta =
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-import-meta'
| |
| );
| |
| | |
| $('<span>')
| |
| .text(label)
| |
| .appendTo(meta);
| |
| | |
| const status =
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-import-status'
| |
| )
| |
| .toggleClass(
| |
| 'is-existing',
| |
| existing
| |
| )
| |
| .toggleClass(
| |
| 'is-new',
| |
| !existing
| |
| )
| |
| .text(
| |
| existing
| |
| ? 'REPLACE EXISTING'
| |
| : 'NEW'
| |
| );
| |
| | |
| content.append(
| |
| header,
| |
| meta,
| |
| status
| |
| );
| |
| | |
| row.append(
| |
| checkbox,
| |
| content
| |
| );
| |
| | |
| list.append(row);
| |
| }
| |
| );
| |
| | |
| box.append(
| |
| list
| |
| );
| |
| | |
| const summary =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-import-summary'
| |
| );
| |
| | |
| const updateSummary =
| |
| () => {
| |
| const selected =
| |
| list.find(
| |
| 'input[type="checkbox"]:checked'
| |
| ).length;
| |
| | |
| const existingSelected =
| |
| list.find(
| |
| 'input[type="checkbox"]:checked'
| |
| ).filter(
| |
| function () {
| |
| const name =
| |
| $(this)
| |
| .attr(
| |
| 'data-import-variable'
| |
| );
| |
| | |
| return !!(
| |
| localFields[
| |
| name
| |
| ]
| |
| );
| |
| }
| |
| ).length;
| |
| | |
| let text =
| |
| selected +
| |
| ' ' +
| |
| (
| |
| selected === 1
| |
| ? 'variable'
| |
| : 'variables'
| |
| ) +
| |
| ' selected.';
| |
| | |
| if (
| |
| existingSelected
| |
| ) {
| |
| text +=
| |
| ' ' +
| |
| existingSelected +
| |
| ' existing ' +
| |
| (
| |
| existingSelected === 1
| |
| ? 'definition'
| |
| : 'definitions'
| |
| ) +
| |
| ' will be replaced.';
| |
| }
| |
| | |
| summary.text(
| |
| text
| |
| );
| |
| | |
| importButton.prop(
| |
| 'disabled',
| |
| selected === 0
| |
| );
| |
| };
| |
| | |
| const filterRows =
| |
| () => {
| |
| const query =
| |
| String(
| |
| search.val() || ''
| |
| )
| |
| .trim()
| |
| .toLowerCase();
| |
| | |
| list
| |
| .find(
| |
| '.preload-editor-import-row'
| |
| )
| |
| .each(
| |
| function () {
| |
| const row =
| |
| $(this);
| |
| | |
| const haystack =
| |
| String(
| |
| row.attr(
| |
| 'data-search'
| |
| ) || ''
| |
| );
| |
| | |
| row.toggle(
| |
| !query ||
| |
| haystack.includes(
| |
| query
| |
| )
| |
| );
| |
| }
| |
| );
| |
| };
| |
| | |
| search.on(
| |
| 'input.preloadEditor',
| |
| filterRows
| |
| );
| |
| | |
| list.on(
| |
| 'change.preloadEditor',
| |
| 'input[type="checkbox"]',
| |
| updateSummary
| |
| );
| |
| | |
| const buttons =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-buttons'
| |
| );
| |
| | |
| const cancel =
| |
| this.createButton(
| |
| 'Cancel',
| |
| '',
| |
| () => {
| |
| overlay.remove();
| |
| resolve(null);
| |
| }
| |
| );
| |
| | |
| const importButton =
| |
| this.createButton(
| |
| 'Import selected',
| |
| 'preload-editor-button-primary',
| |
| () => {
| |
| const selected =
| |
| [];
| |
| | |
| list
| |
| .find(
| |
| 'input[type="checkbox"]:checked'
| |
| )
| |
| .each(
| |
| function () {
| |
| selected.push(
| |
| String(
| |
| $(this)
| |
| .attr(
| |
| 'data-import-variable'
| |
| )
| |
| )
| |
| );
| |
| }
| |
| );
| |
| | |
| overlay.remove();
| |
| resolve(selected);
| |
| }
| |
| );
| |
| | |
| buttons.append(
| |
| cancel,
| |
| importButton
| |
| );
| |
| | |
| box.append(
| |
| summary,
| |
| buttons
| |
| );
| |
| | |
| overlay.append(box);
| |
| | |
| $(document.body)
| |
| .append(overlay);
| |
| | |
| overlay.attr(
| |
| 'tabindex',
| |
| '-1'
| |
| );
| |
| | |
| overlay.trigger('focus');
| |
| | |
| updateSummary();
| |
| | |
| search.trigger('focus');
| |
| | |
| const close =
| |
| event => {
| |
| if (
| |
| event.key ===
| |
| 'Escape'
| |
| ) {
| |
| cancel.trigger(
| |
| 'click'
| |
| );
| |
| }
| |
| };
| |
| | |
| overlay.on(
| |
| 'keydown.preloadEditor',
| |
| close
| |
| );
| |
| | |
| overlay.on(
| |
| 'click.preloadEditor',
| |
| event => {
| |
| if (
| |
| event.target ===
| |
| overlay.get(0)
| |
| ) {
| |
| cancel.trigger(
| |
| 'click'
| |
| );
| |
| }
| |
| }
| |
| );
| |
| }
| |
| );
| |
| },
| |
| | |
| /*
| |
| * Main import workflow.
| |
| */
| |
| async importVariableDefinitions() {
| |
| const source =
| |
| await this.createPromptDialog(
| |
| 'Import variable definitions',
| |
| 'Enter a preload title or its config.json title.'
| |
| );
| |
| | |
| if (!source) {
| |
| return;
| |
| }
| |
| | |
| const configTitle =
| |
| this.normalizeConfigTitle(
| |
| source
| |
| );
| |
| | |
| this.setStatus(
| |
| 'Loading source config...',
| |
| 'loading'
| |
| );
| |
| | |
| try {
| |
| const page =
| |
| await this.getPage(
| |
| configTitle
| |
| );
| |
| | |
| if (!page) {
| |
| throw new Error(
| |
| 'No config.json was found at "' +
| |
| configTitle +
| |
| '".'
| |
| );
| |
| }
| |
| | |
| let sourceConfig;
| |
| | |
| try {
| |
| sourceConfig =
| |
| JSON.parse(
| |
| page.content
| |
| );
| |
| } catch (error) {
| |
| throw new Error(
| |
| 'The source config.json contains invalid JSON: ' +
| |
| error.message
| |
| );
| |
| }
| |
| | |
| if (
| |
| !sourceConfig ||
| |
| typeof sourceConfig !== 'object' ||
| |
| Array.isArray(sourceConfig)
| |
| ) {
| |
| throw new Error(
| |
| 'The source config.json must contain a JSON object.'
| |
| );
| |
| }
| |
| | |
| if (
| |
| !sourceConfig.fields ||
| |
| typeof sourceConfig.fields !== 'object' ||
| |
| Array.isArray(sourceConfig.fields)
| |
| ) {
| |
| throw new Error(
| |
| 'The source config.json does not contain a valid "fields" object.'
| |
| );
| |
| }
| |
| | |
| const fields =
| |
| this.getImportableFields(
| |
| sourceConfig
| |
| );
| |
| | |
| const selected =
| |
| await this.createImportDefinitionsDialog(
| |
| configTitle,
| |
| fields
| |
| );
| |
| | |
| if (
| |
| selected === null
| |
| ) {
| |
| this.setStatus(
| |
| 'Variable import cancelled.',
| |
| 'warning'
| |
| );
| |
| | |
| return;
| |
| }
| |
| | |
| if (!selected.length) {
| |
| this.setStatus(
| |
| 'No variable definitions were imported.',
| |
| 'warning'
| |
| );
| |
| | |
| return;
| |
| }
| |
| | |
| if (
| |
| !this.state.config
| |
| ) {
| |
| this.state.config =
| |
| this.emptyConfig();
| |
| }
| |
| | |
| if (
| |
| !this.state.config.fields
| |
| ) {
| |
| this.state.config.fields = {};
| |
| }
| |
| | |
| const imported = [];
| |
| const replaced = [];
| |
| | |
| selected.forEach(
| |
| name => {
| |
| if (
| |
| name === 'PAGE'
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| const field =
| |
| fields[name];
| |
| | |
| if (
| |
| !field
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| if (
| |
| this.state.config.fields[
| |
| name
| |
| ]
| |
| ) {
| |
| replaced.push(
| |
| name
| |
| );
| |
| } else {
| |
| imported.push(
| |
| name
| |
| );
| |
| }
| |
| | |
| const clone =
| |
| this.cloneConfigField(
| |
| field
| |
| );
| |
| | |
| if (!clone) {
| |
| return;
| |
| }
| |
| | |
| this.state.config.fields[
| |
| name
| |
| ] = clone;
| |
| | |
| /*
| |
| * If this variable was previously deleted,
| |
| * importing its definition explicitly should
| |
| * restore it.
| |
| */
| |
| if (
| |
| Array.isArray(
| |
| this.state.deletedVariables
| |
| )
| |
| ) {
| |
| this.state.deletedVariables =
| |
| this.state.deletedVariables
| |
| .filter(
| |
| deletedName =>
| |
| String(
| |
| deletedName
| |
| ).toUpperCase() !==
| |
| name
| |
| );
| |
| }
| |
| }
| |
| );
| |
| | |
| /*
| |
| * Keep PAGE automatic regardless of what the
| |
| * imported config contained.
| |
| */
| |
| this.state.config.fields.PAGE =
| |
| this.getAutomaticPageField();
| |
| | |
| this.markConfigDirty();
| |
| | |
| /*
| |
| * Keep the current selection when possible.
| |
| * Otherwise select the first imported definition.
| |
| */
| |
| const selectedStillExists =
| |
| !!(
| |
| this.state.config.fields[
| |
| this.state.selectedVariable
| |
| ]
| |
| );
| |
| | |
| if (
| |
| !selectedStillExists ||
| |
| this.state.selectedVariable === 'PAGE'
| |
| ) {
| |
| this.state.selectedVariable =
| |
| selected[0] ||
| |
| 'PAGE';
| |
| }
| |
| | |
| this.reanalyze();
| |
| | |
| this.renderVariablesBySelector();
| |
| this.renderVariableConfigurationBySelector();
| |
| this.renderAnalysisBySelector();
| |
| this.updateEditorTools();
| |
| | |
| this.saveDraft();
| |
| | |
| let status =
| |
| 'Imported ' +
| |
| selected.length +
| |
| ' variable ' +
| |
| (
| |
| selected.length === 1
| |
| ? 'definition'
| |
| : 'definitions'
| |
| ) +
| |
| '.';
| |
| | |
| if (
| |
| replaced.length
| |
| ) {
| |
| status +=
| |
| ' Replaced: ' +
| |
| replaced.join(', ') +
| |
| '.';
| |
| }
| |
| | |
| this.setStatus(
| |
| status,
| |
| 'success'
| |
| );
| |
| | |
| mw.notify(
| |
| status,
| |
| {
| |
| type: 'success'
| |
| }
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor variable import:',
| |
| error
| |
| );
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not import variable definitions.',
| |
| 'error'
| |
| );
| |
| | |
| mw.notify(
| |
| error.message ||
| |
| 'Could not import variable definitions.',
| |
| {
| |
| type: 'error'
| |
| }
| |
| );
| |
| }
| |
| },
| |
| | |
| // =========================================================
| |
| // Preload parsing
| |
| // =========================================================
| |
| | |
| extractIncludeonly(source) {
| |
| const match =
| |
| String(source || '').match(
| |
| /<includeonly>([\s\S]*?)<\/includeonly>/i
| |
| );
| |
| | |
| return match
| |
| ? match[1].trim()
| |
| : String(source || '').trim();
| |
| },
| |
| | |
| findVariables(content) {
| |
| const variables = [];
| |
| const pattern =
| |
| /%%([A-Za-z0-9_-]+)%%/g;
| |
| | |
| let match;
| |
| | |
| while (
| |
| (match = pattern.exec(content)) !== null
| |
| ) {
| |
| const name =
| |
| match[1].toUpperCase();
| |
| | |
| if (!variables.includes(name)) {
| |
| variables.push(name);
| |
| }
| |
| }
| |
| | |
| return variables;
| |
| },
| |
| | |
| findConditions(content) {
| |
| const conditions = [];
| |
| const pattern =
| |
| /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
| |
| | |
| let match;
| |
| | |
| while (
| |
| (match = pattern.exec(content)) !== null
| |
| ) {
| |
| conditions.push({
| |
| type:
| |
| match[1].toLowerCase(),
| |
| | |
| expression:
| |
| match[2].trim(),
| |
| | |
| index:
| |
| match.index
| |
| });
| |
| }
| |
| | |
| return conditions;
| |
| },
| |
| | |
| findConditionVariables(content) {
| |
| const variables = [];
| |
| | |
| this.findConditions(content).forEach(
| |
| condition => {
| |
| if (
| |
| condition.type !== 'if' &&
| |
| condition.type !== 'elseif'
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| const matches =
| |
| condition.expression.match(
| |
| /[A-Za-z0-9_-]+/g
| |
| ) || [];
| |
| | |
| matches.forEach(
| |
| name => {
| |
| name =
| |
| name.toUpperCase();
| |
| | |
| if (
| |
| !variables.includes(
| |
| name
| |
| )
| |
| ) {
| |
| variables.push(
| |
| name
| |
| );
| |
| }
| |
| }
| |
| );
| |
| }
| |
| );
| |
| | |
| return variables;
| |
| },
| |
| | |
| analyzePreload(content) {
| |
| return {
| |
| variables:
| |
| this.findVariables(content),
| |
| | |
| conditions:
| |
| this.findConditions(content),
| |
| | |
| conditionVariables:
| |
| this.findConditionVariables(
| |
| content
| |
| )
| |
| };
| |
| },
| |
| | |
| // =========================================================
| |
| // Config
| |
| // =========================================================
| |
| | |
| getAutomaticPageField() {
| |
| return {
| |
| label: 'Page Name',
| |
| description: 'Name of the page.',
| |
| type: 'page',
| |
| required: true,
| |
| namespace: 'Main',
| |
| 'allow-new': true
| |
| };
| |
| },
| |
| | |
| emptyConfig() {
| |
| return {
| |
| settings: {
| |
| incomplete: 'add-stub'
| |
| },
| |
| | |
| fields: {
| |
| PAGE:
| |
| this.getAutomaticPageField()
| |
| }
| |
| };
| |
| },
| |
| | |
| normalizeConfig(config) {
| |
| if (
| |
| !config ||
| |
| typeof config !== 'object' ||
| |
| Array.isArray(config)
| |
| ) {
| |
| config =
| |
| this.emptyConfig();
| |
| }
| |
| | |
| if (
| |
| !config.settings ||
| |
| typeof config.settings !== 'object' ||
| |
| Array.isArray(config.settings)
| |
| ) {
| |
| config.settings = {};
| |
| }
| |
| | |
| if (
| |
| !config.fields ||
| |
| typeof config.fields !== 'object' ||
| |
| Array.isArray(config.fields)
| |
| ) {
| |
| config.fields = {};
| |
| }
| |
| | |
| config.fields.PAGE =
| |
| this.getAutomaticPageField();
| |
| | |
| return config;
| |
| },
| |
| | |
| createDefaultField(
| |
| name,
| |
| analysis
| |
| ) {
| |
| return {
| |
| label:
| |
| this.humanizeName(name),
| |
| | |
| description: '',
| |
| | |
| type:
| |
| analysis.conditionVariables.includes(
| |
| name
| |
| )
| |
| ? 'boolean'
| |
| : 'text'
| |
| };
| |
| },
| |
| | |
| humanizeName(name) {
| |
| return String(name || '')
| |
| .replace(
| |
| /[_-]+/g,
| |
| ' '
| |
| )
| |
| .replace(
| |
| /([a-z])([A-Z])/g,
| |
| '$1 $2'
| |
| )
| |
| .replace(
| |
| /\s+/g,
| |
| ' '
| |
| )
| |
| .trim()
| |
| .replace(
| |
| /\b\w/g,
| |
| char =>
| |
| char.toUpperCase()
| |
| );
| |
| },
| |
| | |
| prepareConfigForVariables(
| |
| config,
| |
| analysis
| |
| ) {
| |
| config =
| |
| this.normalizeConfig(config);
| |
| | |
| const existing =
| |
| config.fields || {};
| |
| | |
| const deleted =
| |
| new Set(
| |
| Array.isArray(
| |
| this.state.deletedVariables
| |
| )
| |
| ? this.state.deletedVariables
| |
| .map(
| |
| name =>
| |
| String(
| |
| name
| |
| ).toUpperCase()
| |
| )
| |
| : []
| |
| );
| |
| | |
| const fields = {};
| |
| | |
| fields.PAGE =
| |
| this.getAutomaticPageField();
| |
| | |
| analysis.variables.forEach(
| |
| name => {
| |
| if (
| |
| name === 'PAGE' ||
| |
| deleted.has(name)
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| fields[name] =
| |
| existing[name] ||
| |
| this.createDefaultField(
| |
| name,
| |
| analysis
| |
| );
| |
| }
| |
| );
| |
| | |
| Object.keys(existing).forEach(
| |
| name => {
| |
| const upper =
| |
| name.toUpperCase();
| |
| | |
| if (
| |
| upper !== 'PAGE' &&
| |
| !fields[upper] &&
| |
| !deleted.has(upper)
| |
| ) {
| |
| fields[upper] =
| |
| existing[name];
| |
| }
| |
| }
| |
| );
| |
| | |
| config.fields = fields;
| |
| | |
| return config;
| |
| },
| |
| | |
| validateConfig(
| |
| config,
| |
| analysis
| |
| ) {
| |
| const errors = [];
| |
| const warnings = [];
| |
| | |
| if (
| |
| !config ||
| |
| typeof config !== 'object' ||
| |
| Array.isArray(config)
| |
| ) {
| |
| return {
| |
| valid: false,
| |
| errors: [
| |
| 'Config must be an object.'
| |
| ],
| |
| warnings: []
| |
| };
| |
| }
| |
| | |
| if (
| |
| !config.fields ||
| |
| typeof config.fields !== 'object' ||
| |
| Array.isArray(config.fields)
| |
| ) {
| |
| return {
| |
| valid: false,
| |
| errors: [
| |
| '"fields" must be an object.'
| |
| ],
| |
| warnings
| |
| };
| |
| }
| |
| | |
| const configured =
| |
| Object.keys(
| |
| config.fields
| |
| ).map(
| |
| name =>
| |
| name.toUpperCase()
| |
| );
| |
| | |
| analysis.variables.forEach(
| |
| name => {
| |
| if (
| |
| !configured.includes(name)
| |
| ) {
| |
| errors.push(
| |
| 'Variable "' +
| |
| name +
| |
| '" is not configured.'
| |
| );
| |
| }
| |
| }
| |
| );
| |
| | |
| configured.forEach(
| |
| name => {
| |
| if (
| |
| !analysis.variables.includes(
| |
| name
| |
| )
| |
| ) {
| |
| warnings.push(
| |
| 'Field "' +
| |
| name +
| |
| '" is not used by the preload.'
| |
| );
| |
| }
| |
| }
| |
| );
| |
| | |
| if (
| |
| !configured.includes('PAGE')
| |
| ) {
| |
| errors.push(
| |
| 'PAGE must always be configured.'
| |
| );
| |
| }
| |
| | |
| Object.entries(
| |
| config.fields
| |
| ).forEach(
| |
| ([name, field]) => {
| |
| if (
| |
| !field ||
| |
| typeof field !== 'object' ||
| |
| Array.isArray(field)
| |
| ) {
| |
| errors.push(
| |
| 'Field "' +
| |
| name +
| |
| '" must be an object.'
| |
| );
| |
| | |
| return;
| |
| }
| |
| | |
| if (!field.type) {
| |
| warnings.push(
| |
| 'Field "' +
| |
| name +
| |
| '" has no type.'
| |
| );
| |
| }
| |
| | |
| if (
| |
| (
| |
| field.type === 'select' ||
| |
| field.type === 'multiselect'
| |
| ) &&
| |
| !field.source &&
| |
| !Array.isArray(field.options)
| |
| ) {
| |
| warnings.push(
| |
| 'Field "' +
| |
| name +
| |
| '" is a ' +
| |
| field.type +
| |
| ' without a source or manual options.'
| |
| );
| |
| }
| |
| }
| |
| );
| |
| | |
| return {
| |
| valid:
| |
| !errors.length,
| |
| errors,
| |
| warnings
| |
| };
| |
| },
| |
| | |
| // =========================================================
| |
| // Transformation helpers
| |
| // =========================================================
| |
| | |
| transformationObject(
| |
| transform,
| |
| prefix,
| |
| suffix,
| |
| from,
| |
| to
| |
| ) {
| |
| if (!transform) {
| |
| return null;
| |
| }
| |
| | |
| switch (transform) {
| |
| case 'remove-prefix':
| |
| return prefix
| |
| ? {
| |
| transform,
| |
| prefix
| |
| }
| |
| : null;
| |
| | |
| case 'remove-suffix':
| |
| return suffix
| |
| ? {
| |
| transform,
| |
| suffix
| |
| }
| |
| : null;
| |
| | |
| case 'replace':
| |
| return from === ''
| |
| ? null
| |
| : {
| |
| transform,
| |
| from,
| |
| to: to || ''
| |
| };
| |
| | |
| default:
| |
| return null;
| |
| }
| |
| },
| |
| | |
| // =========================================================
| |
| // UI helpers
| |
| // =========================================================
| |
| | |
| createSection(
| |
| title,
| |
| className
| |
| ) {
| |
| const section =
| |
| $('<section>')
| |
| .addClass(
| |
| 'preload-editor-section'
| |
| );
| |
| | |
| if (className) {
| |
| section.addClass(className);
| |
| }
| |
| | |
| $('<h2>')
| |
| .addClass(
| |
| 'preload-editor-section-title'
| |
| )
| |
| .text(title)
| |
| .appendTo(section);
| |
| | |
| return section;
| |
| },
| |
| | |
| createButton(
| |
| label,
| |
| className,
| |
| callback
| |
| ) {
| |
| const button =
| |
| $('<button>')
| |
| .attr(
| |
| 'type',
| |
| 'button'
| |
| )
| |
| .addClass(
| |
| 'preload-editor-button'
| |
| )
| |
| .text(label);
| |
| | |
| if (className) {
| |
| button.addClass(className);
| |
| }
| |
| | |
| if (callback) {
| |
| button.on(
| |
| 'click.preloadEditor',
| |
| callback
| |
| );
| |
| }
| |
| | |
| return button;
| |
| },
| |
| | |
| fieldRow(
| |
| label,
| |
| input,
| |
| description
| |
| ) {
| |
| const row =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-field'
| |
| );
| |
| | |
| $('<label>')
| |
| .addClass(
| |
| 'preload-editor-label'
| |
| )
| |
| .text(label)
| |
| .appendTo(row);
| |
| | |
| if (description) {
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-help'
| |
| )
| |
| .text(description)
| |
| .appendTo(row);
| |
| }
| |
| | |
| row.append(input);
| |
| | |
| return row;
| |
| },
| |
| | |
| textInput(
| |
| value,
| |
| placeholder
| |
| ) {
| |
| return $('<input>')
| |
| .attr({
| |
| type: 'text',
| |
| value:
| |
| value == null
| |
| ? ''
| |
| : value,
| |
| | |
| placeholder:
| |
| placeholder || ''
| |
| })
| |
| .addClass(
| |
| 'preload-editor-input'
| |
| );
| |
| },
| |
| | |
| textarea(value) {
| |
| return $('<textarea>')
| |
| .val(
| |
| value == null
| |
| ? ''
| |
| : value
| |
| )
| |
| .addClass(
| |
| 'preload-editor-textarea'
| |
| );
| |
| },
| |
| | |
| checkbox(checked) {
| |
| return $('<input>')
| |
| .attr(
| |
| 'type',
| |
| 'checkbox'
| |
| )
| |
| .prop(
| |
| 'checked',
| |
| !!checked
| |
| )
| |
| .addClass(
| |
| 'preload-editor-checkbox'
| |
| );
| |
| },
| |
| | |
| select(
| |
| options,
| |
| value
| |
| ) {
| |
| const select =
| |
| $('<select>')
| |
| .addClass(
| |
| 'preload-editor-select'
| |
| );
| |
| | |
| options.forEach(
| |
| option => {
| |
| $('<option>')
| |
| .attr(
| |
| 'value',
| |
| option.value
| |
| )
| |
| .text(
| |
| option.label
| |
| )
| |
| .prop(
| |
| 'selected',
| |
| String(
| |
| option.value
| |
| ) ===
| |
| String(
| |
| value === undefined
| |
| ? ''
| |
| : value
| |
| )
| |
| )
| |
| .appendTo(select);
| |
| }
| |
| );
| |
| | |
| return select;
| |
| },
| |
| | |
| // =========================================================
| |
| // In-wiki dialogs
| |
| // =========================================================
| |
| | |
| createConfirmDialog(
| |
| title,
| |
| message,
| |
| confirmLabel,
| |
| cancelLabel
| |
| ) {
| |
| return new Promise(
| |
| resolve => {
| |
| const overlay =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-overlay'
| |
| );
| |
| | |
| const box =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt'
| |
| );
| |
| | |
| $('<h2>')
| |
| .text(title)
| |
| .appendTo(box);
| |
| | |
| const messageLines =
| |
| String(message || '')
| |
| .split('\n');
| |
| | |
| messageLines.forEach(
| |
| line => {
| |
| if (line === '') {
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-spacer'
| |
| )
| |
| .appendTo(box);
| |
| | |
| return;
| |
| }
| |
| | |
| $('<p>')
| |
| .text(line)
| |
| .appendTo(box);
| |
| }
| |
| );
| |
| | |
| const buttons =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-buttons'
| |
| );
| |
| | |
| const cancel =
| |
| this.createButton(
| |
| cancelLabel || 'Cancel',
| |
| '',
| |
| () => {
| |
| overlay.remove();
| |
| resolve(false);
| |
| }
| |
| );
| |
| | |
| const confirm =
| |
| this.createButton(
| |
| confirmLabel || 'Confirm',
| |
| 'preload-editor-button-primary',
| |
| () => {
| |
| overlay.remove();
| |
| resolve(true);
| |
| }
| |
| );
| |
| | |
| buttons.append(
| |
| cancel,
| |
| confirm
| |
| );
| |
| | |
| box.append(buttons);
| |
| overlay.append(box);
| |
| | |
| $(document.body)
| |
| .append(overlay);
| |
| | |
| const close =
| |
| value => {
| |
| if (value === 'Escape') {
| |
| cancel.trigger('click');
| |
| }
| |
| };
| |
| | |
| overlay.on(
| |
| 'keydown.preloadEditor',
| |
| event => {
| |
| close(event.key);
| |
| }
| |
| );
| |
| | |
| overlay.attr(
| |
| 'tabindex',
| |
| '-1'
| |
| );
| |
| | |
| overlay.trigger('focus');
| |
| | |
| overlay.on(
| |
| 'click.preloadEditor',
| |
| event => {
| |
| if (
| |
| event.target ===
| |
| overlay.get(0)
| |
| ) {
| |
| cancel.trigger('click');
| |
| }
| |
| }
| |
| );
| |
| }
| |
| );
| |
| },
| |
| | |
| createPromptDialog(
| |
| title,
| |
| message
| |
| ) {
| |
| return new Promise(
| |
| resolve => {
| |
| const overlay =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-overlay'
| |
| );
| |
| | |
| const box =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt'
| |
| );
| |
| | |
| $('<h2>')
| |
| .text(title)
| |
| .appendTo(box);
| |
| | |
| $('<p>')
| |
| .text(message)
| |
| .appendTo(box);
| |
| | |
| const input =
| |
| this.textInput();
| |
| | |
| const buttons =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-buttons'
| |
| );
| |
| | |
| const cancel =
| |
| this.createButton(
| |
| 'Cancel',
| |
| '',
| |
| () => {
| |
| overlay.remove();
| |
| resolve(null);
| |
| }
| |
| );
| |
| | |
| const confirm =
| |
| this.createButton(
| |
| 'Load',
| |
| 'preload-editor-button-primary',
| |
| () => {
| |
| const value =
| |
| String(
| |
| input.val() || ''
| |
| ).trim();
| |
| | |
| if (!value) {
| |
| return;
| |
| }
| |
| | |
| overlay.remove();
| |
| resolve(value);
| |
| }
| |
| );
| |
| | |
| buttons.append(
| |
| cancel,
| |
| confirm
| |
| );
| |
| | |
| box.append(
| |
| input,
| |
| buttons
| |
| );
| |
| | |
| overlay.append(box);
| |
| | |
| $(document.body)
| |
| .append(overlay);
| |
| | |
| input.trigger('focus');
| |
| | |
| input.on(
| |
| 'keydown.preloadEditor',
| |
| event => {
| |
| if (
| |
| event.key === 'Enter'
| |
| ) {
| |
| confirm.trigger('click');
| |
| }
| |
| | |
| if (
| |
| event.key === 'Escape'
| |
| ) {
| |
| cancel.trigger('click');
| |
| }
| |
| }
| |
| );
| |
| }
| |
| );
| |
| },
| |
| | |
| createConfigReviewDialog() {
| |
| return new Promise(
| |
| resolve => {
| |
| const overlay =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-overlay preload-editor-code-review-overlay'
| |
| );
| |
| | |
| const box =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt preload-editor-code-review'
| |
| );
| |
| | |
| $('<h2>')
| |
| .text('Review config.json')
| |
| .appendTo(box);
| |
| | |
| $('<p>')
| |
| .text(
| |
| 'This is the config.json that will be saved for the current preload.'
| |
| )
| |
| .appendTo(box);
| |
| | |
| const code =
| |
| $('<pre>')
| |
| .addClass(
| |
| 'preload-editor-config-review'
| |
| )
| |
| .text(
| |
| this.getConfigJSON()
| |
| );
| |
| | |
| const codeWrap =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-config-review-wrap'
| |
| )
| |
| .append(code);
| |
| | |
| box.append(codeWrap);
| |
| | |
| const buttons =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-prompt-buttons'
| |
| );
| |
| | |
| const close =
| |
| this.createButton(
| |
| 'Close',
| |
| 'preload-editor-button-primary',
| |
| () => {
| |
| overlay.remove();
| |
| resolve();
| |
| }
| |
| );
| |
| | |
| buttons.append(close);
| |
| box.append(buttons);
| |
| overlay.append(box);
| |
| | |
| $(document.body)
| |
| .append(overlay);
| |
| | |
| overlay.attr(
| |
| 'tabindex',
| |
| '-1'
| |
| );
| |
| | |
| overlay.trigger('focus');
| |
| | |
| overlay.on(
| |
| 'keydown.preloadEditor',
| |
| event => {
| |
| if (
| |
| event.key === 'Escape'
| |
| ) {
| |
| close.trigger('click');
| |
| }
| |
| }
| |
| );
| |
| | |
| overlay.on(
| |
| 'click.preloadEditor',
| |
| event => {
| |
| if (
| |
| event.target ===
| |
| overlay.get(0)
| |
| ) {
| |
| close.trigger('click');
| |
| }
| |
| }
| |
| );
| |
| }
| |
| );
| |
| },
| |
| | |
| async confirmSave(
| |
| mode,
| |
| title
| |
| ) {
| |
| const configTitle =
| |
| title.replace(
| |
| /\/+$/,
| |
| ''
| |
| ) +
| |
| '/config.json';
| |
| | |
| let saveDescription;
| |
| | |
| if (mode === 'preload') {
| |
| saveDescription =
| |
| 'Save the preload page only?';
| |
| } else if (mode === 'config') {
| |
| saveDescription =
| |
| 'Save the config.json page only?';
| |
| } else {
| |
| saveDescription =
| |
| 'Save both the preload and config.json?';
| |
| }
| |
| | |
| let pages = [];
| |
| | |
| if (
| |
| mode === 'preload' ||
| |
| mode === 'both'
| |
| ) {
| |
| pages.push(
| |
| 'Preload: ' + title
| |
| );
| |
| }
| |
| | |
| if (
| |
| mode === 'config' ||
| |
| mode === 'both'
| |
| ) {
| |
| pages.push(
| |
| 'Config: ' + configTitle
| |
| );
| |
| | |
| pages.push(
| |
| 'Config content model: JSON'
| |
| );
| |
| }
| |
| | |
| const message =
| |
| saveDescription +
| |
| '\n\n' +
| |
| pages.join('\n') +
| |
| '\n\nThis will write the changes to the wiki.';
| |
| | |
| return this.createConfirmDialog(
| |
| 'Confirm save',
| |
| message,
| |
| 'Save',
| |
| 'Cancel'
| |
| );
| |
| },
| |
| | |
| // =========================================================
| |
| // CodeMirror
| |
| // =========================================================
| |
| | |
| destroyEditor() {
| |
| if (!this.editor) {
| |
| this.editorView = null;
| |
| this.editorTools = null;
| |
| return;
| |
| }
| |
| | |
| const {
| |
| view,
| |
| inputHandler
| |
| } = this.editor;
| |
| | |
| if (
| |
| view &&
| |
| inputHandler
| |
| ) {
| |
| view.dom.removeEventListener(
| |
| 'input',
| |
| inputHandler
| |
| );
| |
| }
| |
| | |
| if (view) {
| |
| view.destroy();
| |
| }
| |
| | |
| this.editor = null;
| |
| this.editorView = null;
| |
| this.editorTools = null;
| |
| },
| |
| | |
| getEditorView() {
| |
| return this.editorView;
| |
| },
| |
| | |
| getEditorContent() {
| |
| if (
| |
| this.editorView &&
| |
| this.editorView.state
| |
| ) {
| |
| return this.editorView.state.doc.toString();
| |
| }
| |
| | |
| return (
| |
| this.state.preloadContent || ''
| |
| );
| |
| },
| |
| | |
| setEditorContent(content) {
| |
| content =
| |
| String(content || '');
| |
| | |
| if (!this.editorView) {
| |
| this.state.preloadContent =
| |
| content;
| |
| return;
| |
| }
| |
| | |
| const current =
| |
| this.editorView.state.doc.toString();
| |
| | |
| if (current === content) {
| |
| return;
| |
| }
| |
| | |
| this.editorView.dispatch({
| |
| changes: {
| |
| from: 0,
| |
| to:
| |
| this.editorView.state.doc.length,
| |
| insert: content
| |
| }
| |
| });
| |
| },
| |
| | |
| getEditorSelection() {
| |
| if (
| |
| !this.editorView ||
| |
| !this.editorView.state
| |
| ) {
| |
| return null;
| |
| }
| |
| | |
| const selection =
| |
| this.editorView.state
| |
| .selection.main;
| |
| | |
| return {
| |
| from:
| |
| selection.from,
| |
| | |
| to:
| |
| selection.to,
| |
| | |
| text:
| |
| this.editorView.state.doc.sliceString(
| |
| selection.from,
| |
| selection.to
| |
| )
| |
| };
| |
| },
| |
| | |
| focusEditor() {
| |
| if (this.editorView) {
| |
| this.editorView.focus();
| |
| }
| |
| },
| |
| | |
| insertEditorText(
| |
| text,
| |
| selectionFrom,
| |
| selectionTo,
| |
| cursorPosition
| |
| ) {
| |
| if (!this.editorView) {
| |
| return;
| |
| }
| |
| | |
| const view =
| |
| this.editorView;
| |
| | |
| const selection =
| |
| view.state.selection.main;
| |
| | |
| const from =
| |
| selectionFrom === undefined
| |
| ? selection.from
| |
| : selectionFrom;
| |
| | |
| const to =
| |
| selectionTo === undefined
| |
| ? selection.to
| |
| : selectionTo;
| |
| | |
| const cursor =
| |
| cursorPosition === undefined
| |
| ? from + text.length
| |
| : cursorPosition;
| |
| | |
| view.dispatch({
| |
| changes: {
| |
| from,
| |
| to,
| |
| insert: text
| |
| },
| |
| | |
| selection: {
| |
| anchor: cursor
| |
| }
| |
| });
| |
| | |
| view.focus();
| |
| },
| |
| | |
| // =========================================================
| |
| // Editor tools
| |
| // =========================================================
| |
| | |
| wrapSelectionWithVariable() {
| |
| const selected =
| |
| this.getEditorSelection();
| |
| | |
| if (
| |
| !selected ||
| |
| selected.from === selected.to
| |
| ) {
| |
| this.focusEditor();
| |
| return;
| |
| }
| |
| | |
| const replacement =
| |
| '%%' +
| |
| selected.text +
| |
| '%%';
| |
| | |
| this.insertEditorText(
| |
| replacement,
| |
| selected.from,
| |
| selected.to,
| |
| selected.from +
| |
| replacement.length
| |
| );
| |
| },
| |
| | |
| insertVariable(name) {
| |
| name =
| |
| String(name || '')
| |
| .trim()
| |
| .toUpperCase();
| |
| | |
| if (!name) {
| |
| return;
| |
| }
| |
| | |
| const selected =
| |
| this.getEditorSelection();
| |
| | |
| if (!selected) {
| |
| return;
| |
| }
| |
| | |
| this.insertEditorText(
| |
| '%%' + name + '%%',
| |
| selected.from,
| |
| selected.to
| |
| );
| |
| },
| |
| | |
| insertCondition(
| |
| type,
| |
| variable
| |
| ) {
| |
| type =
| |
| String(type || '')
| |
| .toLowerCase();
| |
| | |
| variable =
| |
| String(variable || '')
| |
| .trim()
| |
| .toUpperCase();
| |
| | |
| const selected =
| |
| this.getEditorSelection();
| |
| | |
| if (!selected) {
| |
| return;
| |
| }
| |
| | |
| let text;
| |
| let cursorOffset;
| |
| | |
| switch (type) {
| |
| case 'if': {
| |
| if (!variable) {
| |
| return;
| |
| }
| |
| | |
| const opening =
| |
| '<!-- @if ' +
| |
| variable +
| |
| ' -->';
| |
| | |
| const closing =
| |
| '<!-- @endif -->';
| |
| | |
| if (
| |
| selected.from !==
| |
| selected.to
| |
| ) {
| |
| text =
| |
| opening +
| |
| '\n' +
| |
| selected.text +
| |
| '\n' +
| |
| closing;
| |
| | |
| cursorOffset =
| |
| text.length;
| |
| } else {
| |
| text =
| |
| opening +
| |
| '\n\n' +
| |
| closing;
| |
| | |
| cursorOffset =
| |
| opening.length + 1;
| |
| }
| |
| | |
| break;
| |
| }
| |
| | |
| case 'elseif':
| |
| text =
| |
| '<!-- @elseif -->';
| |
| cursorOffset =
| |
| text.length;
| |
| break;
| |
| | |
| case 'else':
| |
| text =
| |
| '<!-- @else -->';
| |
| cursorOffset =
| |
| text.length;
| |
| break;
| |
| | |
| case 'endif':
| |
| text =
| |
| '<!-- @endif -->';
| |
| cursorOffset =
| |
| text.length;
| |
| break;
| |
| | |
| default:
| |
| return;
| |
| }
| |
| | |
| this.insertEditorText(
| |
| text,
| |
| selected.from,
| |
| selected.to,
| |
| selected.from +
| |
| cursorOffset
| |
| );
| |
| },
| |
| | |
| getToolVariables() {
| |
| const variables =
| |
| new Set(['PAGE']);
| |
| | |
| if (
| |
| this.analysis &&
| |
| this.analysis.variables
| |
| ) {
| |
| this.analysis.variables.forEach(
| |
| name => {
| |
| variables.add(
| |
| String(
| |
| name
| |
| ).toUpperCase()
| |
| );
| |
| }
| |
| );
| |
| }
| |
| | |
| return Array.from(variables);
| |
| },
| |
| | |
| createToolSelect(
| |
| options,
| |
| className
| |
| ) {
| |
| const select =
| |
| $('<select>')
| |
| .addClass(
| |
| 'preload-editor-tool-select'
| |
| );
| |
| | |
| if (className) {
| |
| select.addClass(className);
| |
| }
| |
| | |
| options.forEach(
| |
| option => {
| |
| $('<option>')
| |
| .attr(
| |
| 'value',
| |
| option.value
| |
| )
| |
| .text(option.label)
| |
| .appendTo(select);
| |
| }
| |
| );
| |
| | |
| return select;
| |
| },
| |
| | |
| updateEditorToolVariables() {
| |
| if (!this.editorTools) {
| |
| return;
| |
| }
| |
| | |
| const variables =
| |
| this.getToolVariables();
| |
| | |
| const current =
| |
| this.editorTools
| |
| .variableSelect
| |
| .val();
| |
| | |
| const currentCondition =
| |
| this.editorTools
| |
| .conditionVariableSelect
| |
| .val();
| |
| | |
| const populate =
| |
| (
| |
| select,
| |
| value
| |
| ) => {
| |
| select.empty();
| |
| | |
| variables.forEach(
| |
| name => {
| |
| $('<option>')
| |
| .attr(
| |
| 'value',
| |
| name
| |
| )
| |
| .text(name)
| |
| .appendTo(select);
| |
| }
| |
| );
| |
| | |
| if (
| |
| value &&
| |
| variables.includes(value)
| |
| ) {
| |
| select.val(value);
| |
| } else if (
| |
| variables.length
| |
| ) {
| |
| select.val(
| |
| variables[0]
| |
| );
| |
| }
| |
| };
| |
| | |
| populate(
| |
| this.editorTools.variableSelect,
| |
| current
| |
| );
| |
| | |
| populate(
| |
| this.editorTools.conditionVariableSelect,
| |
| currentCondition
| |
| );
| |
| },
| |
| | |
| updateConditionVariableVisibility() {
| |
| if (!this.editorTools) {
| |
| return;
| |
| }
| |
| | |
| const type =
| |
| this.editorTools
| |
| .conditionTypeSelect
| |
| .val();
| |
| | |
| this.editorTools
| |
| .conditionVariableWrap
| |
| .toggle(
| |
| type === 'if'
| |
| );
| |
| },
| |
| | |
| renderEditorTools() {
| |
| const tools =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-tools'
| |
| );
| |
| | |
| const wrapGroup =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-tool-group preload-editor-tool-group-wrap'
| |
| );
| |
| | |
| wrapGroup.append(
| |
| this.createButton(
| |
| 'Wrap selection',
| |
| 'preload-editor-tool-button',
| |
| () =>
| |
| this.wrapSelectionWithVariable()
| |
| )
| |
| );
| |
| | |
| const variableGroup =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-tool-group preload-editor-tool-group-variable'
| |
| );
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-tool-label'
| |
| )
| |
| .text(
| |
| 'Insert existing variable'
| |
| )
| |
| .appendTo(variableGroup);
| |
| | |
| const variableSelect =
| |
| this.createToolSelect(
| |
| [],
| |
| 'preload-editor-variable-select'
| |
| );
| |
| | |
| variableGroup.append(
| |
| variableSelect,
| |
| | |
| this.createButton(
| |
| 'Insert',
| |
| 'preload-editor-tool-button',
| |
| () =>
| |
| this.insertVariable(
| |
| variableSelect.val()
| |
| )
| |
| )
| |
| );
| |
| | |
| const conditionGroup =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-tool-group preload-editor-tool-group-condition'
| |
| );
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-tool-label'
| |
| )
| |
| .text('Insert condition')
| |
| .appendTo(conditionGroup);
| |
| | |
| const conditionTypeSelect =
| |
| this.createToolSelect(
| |
| [
| |
| {
| |
| value: 'if',
| |
| label: '@if'
| |
| },
| |
| {
| |
| value: 'elseif',
| |
| label: '@elseif'
| |
| },
| |
| {
| |
| value: 'else',
| |
| label: '@else'
| |
| },
| |
| {
| |
| value: 'endif',
| |
| label: '@endif'
| |
| }
| |
| ],
| |
| 'preload-editor-condition-type-select'
| |
| );
| |
| | |
| const conditionVariableSelect =
| |
| this.createToolSelect(
| |
| [],
| |
| 'preload-editor-condition-variable-select'
| |
| );
| |
| | |
| const conditionVariableWrap =
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-condition-variable'
| |
| )
| |
| .append(
| |
| conditionVariableSelect
| |
| );
| |
| | |
| const insertCondition =
| |
| this.createButton(
| |
| 'Insert',
| |
| 'preload-editor-tool-button',
| |
| () =>
| |
| this.insertCondition(
| |
| conditionTypeSelect.val(),
| |
| conditionVariableSelect.val()
| |
| )
| |
| );
| |
| | |
| conditionTypeSelect.on(
| |
| 'change.preloadEditor',
| |
| () =>
| |
| this.updateConditionVariableVisibility()
| |
| );
| |
| | |
| conditionGroup.append(
| |
| conditionTypeSelect,
| |
| conditionVariableWrap,
| |
| insertCondition
| |
| );
| |
| | |
| tools.append(
| |
| wrapGroup,
| |
| variableGroup,
| |
| conditionGroup
| |
| );
| |
| | |
| this.editorTools = {
| |
| root: tools,
| |
| variableSelect,
| |
| conditionTypeSelect,
| |
| conditionVariableSelect,
| |
| conditionVariableWrap,
| |
| insertCondition
| |
| };
| |
| | |
| this.updateEditorToolVariables();
| |
| this.updateConditionVariableVisibility();
| |
| | |
| return tools;
| |
| },
| |
| | |
| updateEditorTools() {
| |
| this.updateEditorToolVariables();
| |
| this.updateConditionVariableVisibility();
| |
| },
| |
| | |
| // =========================================================
| |
| // Syntax highlighting
| |
| // =========================================================
| |
| | |
| getKnownVariables() {
| |
| const known =
| |
| new Set(['PAGE']);
| |
| | |
| const fields =
| |
| this.state.config &&
| |
| this.state.config.fields;
| |
| | |
| if (
| |
| fields &&
| |
| typeof fields === 'object'
| |
| ) {
| |
| Object.keys(fields).forEach(
| |
| name => {
| |
| known.add(
| |
| String(
| |
| name
| |
| ).toUpperCase()
| |
| );
| |
| }
| |
| );
| |
| }
| |
| | |
| return known;
| |
| },
| |
| | |
| createSyntaxDecorations(
| |
| state,
| |
| Decoration
| |
| ) {
| |
| const decorations = [];
| |
| const source =
| |
| state.doc.toString();
| |
| | |
| const knownVariables =
| |
| this.getKnownVariables();
| |
| | |
| const variablePattern =
| |
| /%%([A-Za-z0-9_-]+)%%/g;
| |
| | |
| let match;
| |
| | |
| while (
| |
| (
| |
| match =
| |
| variablePattern.exec(
| |
| source
| |
| )
| |
| ) !== null
| |
| ) {
| |
| const name =
| |
| match[1].toUpperCase();
| |
| | |
| const known =
| |
| knownVariables.has(name);
| |
| | |
| decorations.push(
| |
| Decoration.mark({
| |
| class: [
| |
| 'preload-editor-syntax-variable',
| |
| known
| |
| ? 'preload-editor-syntax-variable-known'
| |
| : 'preload-editor-syntax-variable-unknown'
| |
| ].join(' ')
| |
| }).range(
| |
| match.index,
| |
| match.index +
| |
| match[0].length
| |
| )
| |
| );
| |
| }
| |
| | |
| const conditionPattern =
| |
| /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
| |
| | |
| while (
| |
| (
| |
| match =
| |
| conditionPattern.exec(
| |
| source
| |
| )
| |
| ) !== null
| |
| ) {
| |
| decorations.push(
| |
| Decoration.mark({
| |
| class:
| |
| 'preload-editor-syntax-condition'
| |
| }).range(
| |
| match.index,
| |
| match.index +
| |
| match[0].length
| |
| )
| |
| );
| |
| | |
| const directiveStart =
| |
| match.index +
| |
| match[0].indexOf('@');
| |
| | |
| decorations.push(
| |
| Decoration.mark({
| |
| class:
| |
| 'preload-editor-syntax-directive'
| |
| }).range(
| |
| directiveStart,
| |
| directiveStart +
| |
| match[1].length +
| |
| 1
| |
| )
| |
| );
| |
| | |
| const type =
| |
| match[1].toLowerCase();
| |
| | |
| if (
| |
| type !== 'if' &&
| |
| type !== 'elseif'
| |
| ) {
| |
| continue;
| |
| }
| |
| | |
| const expression =
| |
| match[2];
| |
| | |
| const expressionStart =
| |
| match.index +
| |
| match[0].indexOf(
| |
| expression
| |
| );
| |
| | |
| const expressionPattern =
| |
| /[A-Za-z0-9_-]+/g;
| |
| | |
| let expressionMatch;
| |
| | |
| while (
| |
| (
| |
| expressionMatch =
| |
| expressionPattern.exec(
| |
| expression
| |
| )
| |
| ) !== null
| |
| ) {
| |
| const name =
| |
| expressionMatch[0]
| |
| .toUpperCase();
| |
| | |
| if (
| |
| !knownVariables.has(name)
| |
| ) {
| |
| continue;
| |
| }
| |
| | |
| const start =
| |
| expressionStart +
| |
| expressionMatch.index;
| |
| | |
| decorations.push(
| |
| Decoration.mark({
| |
| class:
| |
| 'preload-editor-syntax-condition-variable'
| |
| }).range(
| |
| start,
| |
| start +
| |
| expressionMatch[0]
| |
| .length
| |
| )
| |
| );
| |
| }
| |
| }
| |
| | |
| return Decoration.set(
| |
| decorations,
| |
| true
| |
| );
| |
| },
| |
| | |
| async installSyntaxHighlighting(view) {
| |
| await mw.loader.using(
| |
| 'ext.CodeMirror.v6.lib'
| |
| );
| |
| | |
| const require =
| |
| mw.loader.require;
| |
| | |
| const {
| |
| StateField,
| |
| StateEffect,
| |
| EditorView,
| |
| Decoration
| |
| } = require(
| |
| 'ext.CodeMirror.v6.lib'
| |
| );
| |
| | |
| const syntaxField =
| |
| StateField.define({
| |
| create: state =>
| |
| this.createSyntaxDecorations(
| |
| state,
| |
| Decoration
| |
| ),
| |
| | |
| update: (
| |
| decorations,
| |
| transaction
| |
| ) =>
| |
| transaction.docChanged
| |
| ? this.createSyntaxDecorations(
| |
| transaction.state,
| |
| Decoration
| |
| )
| |
| : decorations,
| |
| | |
| provide: field =>
| |
| EditorView.decorations.from(
| |
| field
| |
| )
| |
| });
| |
| | |
| view.dispatch({
| |
| effects:
| |
| StateEffect.appendConfig.of(
| |
| syntaxField
| |
| )
| |
| });
| |
| | |
| this.editor.syntaxField =
| |
| syntaxField;
| |
| },
| |
| | |
| async mountCodeMirror(textarea) {
| |
| return new Promise(
| |
| (
| |
| resolve,
| |
| reject
| |
| ) => {
| |
| let settled = false;
| |
| | |
| const cleanup =
| |
| () => {
| |
| mw.hook(
| |
| 'dev.CCM.ready'
| |
| ).remove(onReady);
| |
| | |
| mw.hook(
| |
| 'dev.CCM.load'
| |
| ).remove(onLoad);
| |
| };
| |
| | |
| const onReady =
| |
| async cm => {
| |
| if (
| |
| settled ||
| |
| !cm ||
| |
| cm.textarea !== textarea ||
| |
| !cm.view
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| settled = true;
| |
| cleanup();
| |
| | |
| const view =
| |
| cm.view;
| |
| | |
| const inputHandler =
| |
| () => {
| |
| this.state
| |
| .preloadContent =
| |
| view.state.doc.toString();
| |
| | |
| this.markPreloadDirty();
| |
| | |
| this.reanalyze();
| |
| | |
| this.renderVariablesBySelector();
| |
| this.renderVariableConfigurationBySelector();
| |
| this.renderAnalysisBySelector();
| |
| this.updateEditorTools();
| |
| | |
| this.saveDraft();
| |
| };
| |
| | |
| view.dom.addEventListener(
| |
| 'input',
| |
| inputHandler
| |
| );
| |
| | |
| view.dom.classList.add(
| |
| 'preload-editor-codemirror'
| |
| );
| |
| | |
| const wrapper =
| |
| view.dom.closest(
| |
| '.ext-codemirror-wrapper'
| |
| );
| |
| | |
| if (wrapper) {
| |
| wrapper.classList.add(
| |
| 'preload-editor-codemirror-wrapper'
| |
| );
| |
| }
| |
| | |
| this.editor = {
| |
| cm,
| |
| view,
| |
| textarea,
| |
| inputHandler
| |
| };
| |
| | |
| this.editorView =
| |
| view;
| |
| | |
| this.state.preloadContent =
| |
| view.state.doc.toString();
| |
| | |
| try {
| |
| await this.installSyntaxHighlighting(
| |
| view
| |
| );
| |
| | |
| if (
| |
| this.state.dirty
| |
| ) {
| |
| this.saveDraft();
| |
| }
| |
| | |
| resolve(view);
| |
| } catch (error) {
| |
| reject(error);
| |
| }
| |
| };
| |
| | |
| const onLoad =
| |
| loader => {
| |
| if (
| |
| settled ||
| |
| typeof loader !==
| |
| 'function'
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| try {
| |
| loader(textarea);
| |
| } catch (error) {
| |
| settled = true;
| |
| cleanup();
| |
| reject(error);
| |
| }
| |
| };
| |
| | |
| mw.hook(
| |
| 'dev.CCM.ready'
| |
| ).add(onReady);
| |
| | |
| mw.hook(
| |
| 'dev.CCM.load'
| |
| ).add(onLoad);
| |
| }
| |
| );
| |
| },
| |
| | |
| // =========================================================
| |
| // Main UI
| |
| // =========================================================
| |
| | |
| init() {
| |
| const root =
| |
| $('#mw-content-text');
| |
| | |
| if (!root.length) {
| |
| return;
| |
| }
| |
| | |
| this.root =
| |
| $('<div>')
| |
| .attr(
| |
| 'id',
| |
| 'preload-editor'
| |
| )
| |
| .addClass(
| |
| 'preload-editor'
| |
| );
| |
| | |
| root.append(this.root);
| |
| | |
| this.renderShell();
| |
| | |
| this.restoreDraft();
| |
| | |
| window.addEventListener(
| |
| 'beforeunload',
| |
| () => {
| |
| if (this.state.dirty) {
| |
| this.saveDraft();
| |
| }
| |
| }
| |
| );
| |
| },
| |
| | |
| renderShell() {
| |
| this.destroyEditor();
| |
| this.root.empty();
| |
| | |
| const loader =
| |
| this.createSection('Preload');
| |
| | |
| const titleInput =
| |
| this.textInput(
| |
| this.state.preloadTitle,
| |
| 'Template:Example/Preload'
| |
| ).attr(
| |
| 'id',
| |
| 'preload-editor-title-input'
| |
| );
| |
| | |
| titleInput.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| this.state.preloadTitle =
| |
| String(
| |
| titleInput.val() || ''
| |
| ).trim();
| |
| | |
| if (
| |
| this.state.dirty &&
| |
| this.state.mode === 'new'
| |
| ) {
| |
| this.saveDraft();
| |
| }
| |
| }
| |
| );
| |
| | |
| const loadButton =
| |
| this.createButton(
| |
| 'Load',
| |
| 'preload-editor-button-primary',
| |
| () =>
| |
| this.loadExisting(
| |
| titleInput.val()
| |
| )
| |
| );
| |
| | |
| const newButton =
| |
| this.createButton(
| |
| 'New',
| |
| '',
| |
| () => this.startNew()
| |
| );
| |
| | |
| const sampleButton =
| |
| this.createButton(
| |
| 'Load Sample Page',
| |
| '',
| |
| () => this.loadSamplePage()
| |
| );
| |
| | |
| loader.append(
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-toolbar'
| |
| )
| |
| .append(
| |
| titleInput,
| |
| loadButton,
| |
| newButton,
| |
| sampleButton
| |
| )
| |
| );
| |
| | |
| this.root.append(loader);
| |
| | |
| this.workspace =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-workspace'
| |
| );
| |
| | |
| this.root.append(
| |
| this.workspace
| |
| );
| |
| | |
| /*
| |
| * Fixed footer
| |
| *
| |
| * Local draft status, normal status messages,
| |
| * and save actions live here.
| |
| */
| |
| const footer =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-footer'
| |
| );
| |
| | |
| this.localDraftStatus =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-local-draft-status is-empty'
| |
| )
| |
| .text(
| |
| 'No local draft'
| |
| );
| |
| | |
| this.status =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-status'
| |
| );
| |
| | |
| footer.append(
| |
| this.localDraftStatus,
| |
| this.status
| |
| );
| |
| | |
| this.root.append(
| |
| footer
| |
| );
| |
| | |
| this.updateLocalDraftIndicator();
| |
| | |
| this.renderWelcome();
| |
| },
| |
| | |
| renderWelcome() {
| |
| this.workspace.empty();
| |
| | |
| const section =
| |
| this.createSection('Get started');
| |
| | |
| $('<p>')
| |
| .text(
| |
| 'Load an existing preload, create a new one, or start from a sample wiki page.'
| |
| )
| |
| .appendTo(section);
| |
| | |
| const tips =
| |
| $('<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(
| |
| text => {
| |
| $('<li>')
| |
| .text(text)
| |
| .appendTo(tips);
| |
| }
| |
| );
| |
| | |
| section.append(tips);
| |
| this.workspace.append(section);
| |
| },
| |
| | |
| setStatus(
| |
| message,
| |
| type
| |
| ) {
| |
| if (!this.status) {
| |
| return;
| |
| }
| |
| | |
| this.status.removeClass(
| |
| 'is-error',
| |
| 'is-success',
| |
| 'is-warning',
| |
| 'is-loading'
| |
| );
| |
| | |
| if (type) {
| |
| this.status.addClass(
| |
| 'is-' + type
| |
| );
| |
| }
| |
| | |
| this.status.text(
| |
| message || ''
| |
| );
| |
| },
| |
| | |
| // =========================================================
| |
| // Load / create
| |
| // =========================================================
| |
| | |
| async loadExisting(title) {
| |
| title =
| |
| String(title || '').trim();
| |
| | |
| if (!title) {
| |
| mw.notify(
| |
| 'Enter a preload page title first.',
| |
| {
| |
| type: 'warn'
| |
| }
| |
| );
| |
| | |
| return;
| |
| }
| |
| | |
| this.setStatus(
| |
| 'Loading preload...',
| |
| 'loading'
| |
| );
| |
| | |
| try {
| |
| const page =
| |
| await this.getPage(title);
| |
| | |
| if (!page) {
| |
| const suggestions =
| |
| await this.findSimilarPreloads(
| |
| title
| |
| );
| |
| | |
| this.renderMissingPage(
| |
| title,
| |
| suggestions
| |
| );
| |
| | |
| this.setStatus(
| |
| 'Preload not found.',
| |
| 'warning'
| |
| );
| |
| | |
| return;
| |
| }
| |
| | |
| await this.loadDefinition(
| |
| page.title,
| |
| page.content
| |
| );
| |
| | |
| this.setStatus(
| |
| 'Preload loaded.',
| |
| 'success'
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor:',
| |
| error
| |
| );
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not load preload.',
| |
| 'error'
| |
| );
| |
| }
| |
| },
| |
| | |
| async findSimilarPreloads(title) {
| |
| let query =
| |
| title
| |
| .replace(
| |
| /\/Preload$/i,
| |
| ''
| |
| )
| |
| .trim();
| |
| | |
| if (!query) {
| |
| query = title;
| |
| }
| |
| | |
| try {
| |
| const results =
| |
| await this.searchPages(query);
| |
| | |
| return results.filter(
| |
| result =>
| |
| /\/Preload$/i.test(
| |
| result.title
| |
| )
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor search:',
| |
| error
| |
| );
| |
| | |
| return [];
| |
| }
| |
| },
| |
| | |
| renderMissingPage(
| |
| title,
| |
| suggestions
| |
| ) {
| |
| this.workspace.empty();
| |
| | |
| const section =
| |
| this.createSection(
| |
| 'Preload not found'
| |
| );
| |
| | |
| $('<p>')
| |
| .text(
| |
| 'No preload exists at "' +
| |
| title +
| |
| '".'
| |
| )
| |
| .appendTo(section);
| |
| | |
| if (suggestions.length) {
| |
| $('<h3>')
| |
| .text('Similar preloads')
| |
| .appendTo(section);
| |
| | |
| const list =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-suggestions'
| |
| );
| |
| | |
| suggestions.forEach(
| |
| result => {
| |
| list.append(
| |
| this.createButton(
| |
| result.title,
| |
| 'preload-editor-suggestion',
| |
| () =>
| |
| this.loadExisting(
| |
| result.title
| |
| )
| |
| )
| |
| );
| |
| }
| |
| );
| |
| | |
| section.append(list);
| |
| }
| |
| | |
| section.append(
| |
| this.createButton(
| |
| 'Create this preload',
| |
| 'preload-editor-button-primary',
| |
| () =>
| |
| this.startNew(title)
| |
| )
| |
| );
| |
| | |
| this.workspace.append(section);
| |
| },
| |
| | |
| async startNew(title) {
| |
| title =
| |
| String(title || '').trim();
| |
| | |
| this.state = {
| |
| preloadTitle: title,
| |
| preloadSource: '',
| |
| preloadContent:
| |
| this.defaults.preload,
| |
| | |
| config:
| |
| this.emptyConfig(),
| |
| | |
| variables: ['PAGE'],
| |
| conditions: [],
| |
| selectedVariable: 'PAGE',
| |
| deletedVariables: [],
| |
| mode: 'new',
| |
| dirty: true,
| |
| dirtyPreload: true,
| |
| dirtyConfig: true,
| |
| | |
| localDraftSavedAt: null,
| |
| localDraftAvailable: false
| |
| };
| |
| | |
| this.saveDraft();
| |
| this.renderEditor();
| |
| | |
| this.setStatus(
| |
| 'Creating a new preload.',
| |
| 'success'
| |
| );
| |
| },
| |
| | |
| async loadSamplePage() {
| |
| const result =
| |
| await this.createPromptDialog(
| |
| 'Load Sample Page',
| |
| 'Enter the title of a normal wiki page to use as the starting point.'
| |
| );
| |
| | |
| if (!result) {
| |
| return;
| |
| }
| |
| | |
| this.setStatus(
| |
| 'Loading sample page...',
| |
| 'loading'
| |
| );
| |
| | |
| try {
| |
| const page =
| |
| await this.getPage(result);
| |
| | |
| if (!page) {
| |
| throw new Error(
| |
| 'Sample page not found: ' +
| |
| result
| |
| );
| |
| }
| |
| | |
| const sample =
| |
| this.convertSampleToPreload(
| |
| page.content
| |
| );
| |
| | |
| this.state = {
| |
| preloadTitle: '',
| |
| preloadSource: '',
| |
| preloadContent: sample,
| |
| | |
| config:
| |
| this.emptyConfig(),
| |
| | |
| variables: ['PAGE'],
| |
| conditions: [],
| |
| selectedVariable: 'PAGE',
| |
| deletedVariables: [],
| |
| mode: 'new',
| |
| dirty: true,
| |
| dirtyPreload: true,
| |
| dirtyConfig: true,
| |
| | |
| localDraftSavedAt: null,
| |
| localDraftAvailable: false
| |
| };
| |
| | |
| this.reanalyze();
| |
| this.saveDraft();
| |
| this.renderEditor();
| |
| | |
| this.setStatus(
| |
| 'Sample page loaded.',
| |
| 'success'
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor sample:',
| |
| error
| |
| );
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not load sample page.',
| |
| 'error'
| |
| );
| |
| }
| |
| },
| |
| | |
| convertSampleToPreload(source) {
| |
| let content =
| |
| String(source || '').trim();
| |
| | |
| const include =
| |
| content.match(
| |
| /<includeonly>([\s\S]*?)<\/includeonly>/i
| |
| );
| |
| | |
| if (include) {
| |
| content =
| |
| include[1].trim();
| |
| }
| |
| | |
| content =
| |
| content
| |
| .replace(
| |
| /<noinclude>[\s\S]*?<\/noinclude>/gi,
| |
| ''
| |
| )
| |
| .trim();
| |
| | |
| return (
| |
| '<includeonly>\n' +
| |
| content +
| |
| '\n</includeonly>\n' +
| |
| '<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
| |
| );
| |
| },
| |
| | |
| async loadDefinition(
| |
| title,
| |
| source
| |
| ) {
| |
| const content =
| |
| this.extractIncludeonly(source);
| |
| | |
| let config =
| |
| this.emptyConfig();
| |
| | |
| const configTitle =
| |
| title.replace(
| |
| /\/+$/,
| |
| ''
| |
| ) +
| |
| '/config.json';
| |
| | |
| const configPage =
| |
| await this.getPage(configTitle);
| |
| | |
| if (configPage) {
| |
| try {
| |
| config =
| |
| JSON.parse(
| |
| configPage.content
| |
| );
| |
| } catch (error) {
| |
| throw new Error(
| |
| 'The existing config.json contains invalid JSON: ' +
| |
| error.message
| |
| );
| |
| }
| |
| }
| |
| | |
| const analysis =
| |
| this.analyzePreload(content);
| |
| | |
| this.state.deletedVariables = [];
| |
| | |
| config =
| |
| this.prepareConfigForVariables(
| |
| config,
| |
| analysis
| |
| );
| |
| | |
| this.state = {
| |
| preloadTitle: title,
| |
| preloadSource: source,
| |
| preloadContent: source,
| |
| config,
| |
| | |
| variables:
| |
| analysis.variables,
| |
| | |
| conditions:
| |
| analysis.conditions,
| |
| | |
| selectedVariable:
| |
| analysis.variables.includes('PAGE')
| |
| ? 'PAGE'
| |
| : (
| |
| analysis.variables[0] ||
| |
| 'PAGE'
| |
| ),
| |
| | |
| deletedVariables: [],
| |
| | |
| mode: 'edit',
| |
| dirty: false,
| |
| dirtyPreload: false,
| |
| dirtyConfig: false,
| |
| | |
| localDraftSavedAt: null,
| |
| localDraftAvailable: false
| |
| };
| |
| | |
| this.clearDraft(title);
| |
| this.renderEditor();
| |
| },
| |
| | |
| // =========================================================
| |
| // Editor
| |
| // =========================================================
| |
| | |
| renderEditor() {
| |
| this.destroyEditor();
| |
| this.workspace.empty();
| |
| | |
| const layout =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-layout'
| |
| );
| |
| | |
| const left =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-left'
| |
| );
| |
| | |
| const right =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-right'
| |
| );
| |
| | |
| const editorSection =
| |
| this.createSection(
| |
| 'Preload',
| |
| 'preload-editor-main-section'
| |
| );
| |
| | |
| const meta =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-meta'
| |
| );
| |
| | |
| $('<strong>')
| |
| .text(
| |
| this.state.preloadTitle ||
| |
| 'New preload'
| |
| )
| |
| .appendTo(meta);
| |
| | |
| if (
| |
| this.state.mode === 'new'
| |
| ) {
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge'
| |
| )
| |
| .text('NEW')
| |
| .appendTo(meta);
| |
| }
| |
| | |
| editorSection.append(meta);
| |
| editorSection.append(
| |
| this.renderEditorTools()
| |
| );
| |
| | |
| const textarea =
| |
| this.textarea(
| |
| this.state.preloadContent
| |
| )
| |
| .attr(
| |
| 'id',
| |
| 'preload-editor-content'
| |
| )
| |
| .addClass(
| |
| 'preload-editor-content'
| |
| );
| |
| | |
| const editorMount =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-codemirror-mount'
| |
| )
| |
| .append(textarea);
| |
| | |
| editorSection.append(editorMount);
| |
| left.append(editorSection);
| |
| | |
| const analysisSection =
| |
| this.createSection('Analysis');
| |
| | |
| const analysisContainer =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-analysis'
| |
| );
| |
| | |
| analysisSection.append(
| |
| analysisContainer
| |
| );
| |
| | |
| const variablesSection =
| |
| this.createSection('Variables');
| |
| | |
| const detectVariablesButton =
| |
| this.createButton(
| |
| 'Detect variables',
| |
| 'preload-editor-button-primary preload-editor-detect-variables',
| |
| () =>
| |
| this.detectVariables()
| |
| );
| |
| | |
| const detectHelp =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-help preload-editor-detect-variables-help'
| |
| )
| |
| .text(
| |
| 'Refresh the variable list after editing the preload.'
| |
| );
| |
| | |
| variablesSection.append(
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-detect'
| |
| )
| |
| .append(
| |
| detectVariablesButton,
| |
| detectHelp
| |
| )
| |
| );
| |
| | |
| const variableContainer =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variables'
| |
| );
| |
| | |
| variablesSection.append(
| |
| variableContainer
| |
| );
| |
| | |
| const configurationSection =
| |
| this.createSection(
| |
| 'Variable configuration'
| |
| );
| |
| | |
| /*
| |
| * Import button lives at the top of the
| |
| * variable configuration section so the
| |
| * workflow is always easy to find.
| |
| */
| |
| const importDefinitionsButton =
| |
| this.createButton(
| |
| 'Import definitions',
| |
| 'preload-editor-button-primary preload-editor-import-definitions',
| |
| () =>
| |
| this.importVariableDefinitions()
| |
| );
| |
| | |
| const importHelp =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-help preload-editor-import-definitions-help'
| |
| )
| |
| .text(
| |
| 'Import variable definitions from another preload config.json. Existing definitions can be replaced.'
| |
| );
| |
| | |
| const importControls =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-import-definition-controls'
| |
| )
| |
| .append(
| |
| importDefinitionsButton,
| |
| importHelp
| |
| );
| |
| | |
| configurationSection.append(
| |
| importControls
| |
| );
| |
| | |
| const configurationContainer =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-configuration'
| |
| );
| |
| | |
| configurationSection.append(
| |
| configurationContainer
| |
| );
| |
| | |
| const configSection =
| |
| this.createSection(
| |
| 'Configuration'
| |
| );
| |
| | |
| const configHelp =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-help'
| |
| )
| |
| .text(
| |
| 'Review the exact config.json that will be uploaded to the wiki.'
| |
| );
| |
| | |
| const reviewConfigButton =
| |
| this.createButton(
| |
| 'Review config.json',
| |
| '',
| |
| () =>
| |
| this.createConfigReviewDialog()
| |
| );
| |
| | |
| configSection.append(
| |
| configHelp,
| |
| reviewConfigButton
| |
| );
| |
| | |
| right.append(
| |
| analysisSection,
| |
| variablesSection,
| |
| configurationSection,
| |
| configSection
| |
| );
| |
| | |
| layout.append(
| |
| left,
| |
| right
| |
| );
| |
| | |
| const saveBar =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-save-bar'
| |
| );
| |
| | |
| saveBar.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(saveBar);
| |
| | |
| this.workspace.append(
| |
| layout
| |
| );
| |
| | |
| this.reanalyze();
| |
| | |
| this.renderVariables(
| |
| variableContainer
| |
| );
| |
| | |
| this.renderAnalysis(
| |
| analysisContainer
| |
| );
| |
| | |
| this.renderVariableConfiguration(
| |
| configurationContainer
| |
| );
| |
| | |
| this.updateEditorTools();
| |
| this.updateLocalDraftIndicator();
| |
| | |
| this.mountCodeMirror(
| |
| textarea.get(0)
| |
| ).catch(
| |
| error => {
| |
| console.error(
| |
| 'Preload Editor CodeMirror:',
| |
| error
| |
| );
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not initialize CodeMirror.',
| |
| 'error'
| |
| );
| |
| }
| |
| );
| |
| },
| |
| | |
| // =========================================================
| |
| // Variable detection
| |
| // =========================================================
| |
| | |
| detectVariables() {
| |
| const source =
| |
| this.getEditorContent();
| |
| | |
| this.state.preloadContent =
| |
| source;
| |
| | |
| const content =
| |
| this.extractIncludeonly(
| |
| source
| |
| );
| |
| | |
| const analysis =
| |
| this.analyzePreload(
| |
| content
| |
| );
| |
| | |
| /*
| |
| * If a variable was previously deleted from
| |
| * the configuration, detecting the current
| |
| * preload again should allow it to come back.
| |
| */
| |
| if (
| |
| Array.isArray(
| |
| this.state.deletedVariables
| |
| )
| |
| ) {
| |
| const detected =
| |
| new Set(
| |
| analysis.variables
| |
| .map(
| |
| name =>
| |
| String(
| |
| name
| |
| ).toUpperCase()
| |
| )
| |
| );
| |
| | |
| this.state.deletedVariables =
| |
| this.state.deletedVariables
| |
| .filter(
| |
| name =>
| |
| !detected.has(
| |
| String(
| |
| name
| |
| ).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 source =
| |
| this.getEditorContent();
| |
| | |
| const content =
| |
| this.extractIncludeonly(source);
| |
| | |
| const analysis =
| |
| this.analyzePreload(
| |
| content
| |
| );
| |
| | |
| this.state.preloadContent =
| |
| source;
| |
| | |
| this.state.variables =
| |
| analysis.variables;
| |
| | |
| this.state.conditions =
| |
| analysis.conditions;
| |
| | |
| this.state.config =
| |
| this.prepareConfigForVariables(
| |
| this.state.config ||
| |
| this.emptyConfig(),
| |
| analysis
| |
| );
| |
| | |
| this.analysis =
| |
| analysis;
| |
| | |
| const available =
| |
| ['PAGE'].concat(
| |
| analysis.variables.filter(
| |
| name =>
| |
| name !== 'PAGE'
| |
| )
| |
| );
| |
| | |
| if (
| |
| !available.includes(
| |
| this.state.selectedVariable
| |
| )
| |
| ) {
| |
| this.state.selectedVariable =
| |
| available[0] ||
| |
| 'PAGE';
| |
| }
| |
| },
| |
| | |
| // =========================================================
| |
| // Analysis
| |
| // =========================================================
| |
| | |
| renderAnalysis(container) {
| |
| container.empty();
| |
| | |
| const analysis =
| |
| this.analysis ||
| |
| this.analyzePreload(
| |
| this.extractIncludeonly(
| |
| this.getEditorContent()
| |
| )
| |
| );
| |
| | |
| const variableCount =
| |
| analysis.variables.length;
| |
| | |
| const conditionCount =
| |
| analysis.conditions.length;
| |
| | |
| const configuredCount =
| |
| Object.keys(
| |
| this.state.config &&
| |
| this.state.config.fields
| |
| ? this.state.config.fields
| |
| : {}
| |
| ).length;
| |
| | |
| const summary =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-analysis-summary'
| |
| );
| |
| | |
| const createMetric =
| |
| (
| |
| value,
| |
| label
| |
| ) =>
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-analysis-metric'
| |
| )
| |
| .append(
| |
| $('<strong>')
| |
| .addClass(
| |
| 'preload-editor-analysis-value'
| |
| )
| |
| .text(value),
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-analysis-label'
| |
| )
| |
| .text(label)
| |
| );
| |
| | |
| summary.append(
| |
| createMetric(
| |
| variableCount,
| |
| variableCount === 1
| |
| ? 'variable'
| |
| : 'variables'
| |
| ),
| |
| | |
| createMetric(
| |
| conditionCount,
| |
| conditionCount === 1
| |
| ? 'condition marker'
| |
| : 'condition markers'
| |
| ),
| |
| | |
| createMetric(
| |
| configuredCount,
| |
| configuredCount === 1
| |
| ? 'configured field'
| |
| : 'configured fields'
| |
| )
| |
| );
| |
| | |
| container.append(summary);
| |
| | |
| if (
| |
| analysis.conditionVariables.length
| |
| ) {
| |
| container.append(
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-analysis-note'
| |
| )
| |
| .text(
| |
| 'Condition variables: ' +
| |
| analysis.conditionVariables.join(', ')
| |
| )
| |
| );
| |
| }
| |
| | |
| const validation =
| |
| this.validateConfig(
| |
| this.state.config,
| |
| analysis
| |
| );
| |
| | |
| if (validation.errors.length) {
| |
| const errors =
| |
| $('<ul>')
| |
| .addClass(
| |
| 'preload-editor-validation-errors'
| |
| );
| |
| | |
| validation.errors.forEach(
| |
| error => {
| |
| $('<li>')
| |
| .text(error)
| |
| .appendTo(errors);
| |
| }
| |
| );
| |
| | |
| container.append(errors);
| |
| }
| |
| | |
| if (validation.warnings.length) {
| |
| const warnings =
| |
| $('<ul>')
| |
| .addClass(
| |
| 'preload-editor-validation-warnings'
| |
| );
| |
| | |
| validation.warnings.forEach(
| |
| warning => {
| |
| $('<li>')
| |
| .text(warning)
| |
| .appendTo(warnings);
| |
| }
| |
| );
| |
| | |
| container.append(warnings);
| |
| }
| |
| | |
| if (
| |
| !validation.errors.length &&
| |
| !validation.warnings.length
| |
| ) {
| |
| container.append(
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-validation-success'
| |
| )
| |
| .text(
| |
| 'Preload and configuration are in sync.'
| |
| )
| |
| );
| |
| }
| |
| },
| |
| | |
| renderAnalysisBySelector() {
| |
| const container =
| |
| this.root &&
| |
| this.root.find(
| |
| '.preload-editor-analysis'
| |
| );
| |
| | |
| if (
| |
| container &&
| |
| container.length
| |
| ) {
| |
| this.renderAnalysis(container);
| |
| }
| |
| },
| |
| | |
| // =========================================================
| |
| // Variables
| |
| // =========================================================
| |
| | |
| renderVariables(container) {
| |
| container.empty();
| |
| | |
| const analysis =
| |
| this.analysis ||
| |
| this.analyzePreload(
| |
| this.extractIncludeonly(
| |
| this.getEditorContent()
| |
| )
| |
| );
| |
| | |
| const variables =
| |
| analysis.variables.slice();
| |
| | |
| const ordered =
| |
| ['PAGE'];
| |
| | |
| variables.forEach(
| |
| name => {
| |
| if (
| |
| name !== 'PAGE' &&
| |
| !ordered.includes(name)
| |
| ) {
| |
| ordered.push(name);
| |
| }
| |
| }
| |
| );
| |
| | |
| const configuredFields =
| |
| this.state.config &&
| |
| this.state.config.fields
| |
| ? Object.keys(
| |
| this.state.config.fields
| |
| )
| |
| : [];
| |
| | |
| configuredFields.forEach(
| |
| name => {
| |
| const upper =
| |
| name.toUpperCase();
| |
| | |
| if (
| |
| upper !== 'PAGE' &&
| |
| !ordered.includes(upper)
| |
| ) {
| |
| ordered.push(upper);
| |
| }
| |
| }
| |
| );
| |
| | |
| const deleted =
| |
| Array.isArray(
| |
| this.state.deletedVariables
| |
| )
| |
| ? this.state.deletedVariables
| |
| : [];
| |
| | |
| deleted.forEach(
| |
| name => {
| |
| const upper =
| |
| String(name).toUpperCase();
| |
| | |
| if (
| |
| upper !== 'PAGE' &&
| |
| !ordered.includes(upper)
| |
| ) {
| |
| ordered.push(upper);
| |
| }
| |
| }
| |
| );
| |
| | |
| const list =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-list'
| |
| );
| |
| | |
| ordered.forEach(
| |
| name => {
| |
| const isSelected =
| |
| name ===
| |
| this.state.selectedVariable;
| |
| | |
| const isUsed =
| |
| analysis.variables.includes(
| |
| name
| |
| );
| |
| | |
| const isConfigured =
| |
| !!(
| |
| this.state.config &&
| |
| this.state.config.fields &&
| |
| this.state.config.fields[name]
| |
| );
| |
| | |
| const item =
| |
| $('<button>')
| |
| .attr({
| |
| type: 'button',
| |
| 'data-variable':
| |
| name
| |
| })
| |
| .addClass(
| |
| 'preload-editor-variable-item'
| |
| )
| |
| .toggleClass(
| |
| 'is-selected',
| |
| isSelected
| |
| )
| |
| .on(
| |
| 'click.preloadEditor',
| |
| () =>
| |
| this.selectVariable(
| |
| name
| |
| )
| |
| );
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-variable-name'
| |
| )
| |
| .text(name)
| |
| .appendTo(item);
| |
| | |
| if (name === 'PAGE') {
| |
| item.append(
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge'
| |
| )
| |
| .text('AUTOMATIC')
| |
| );
| |
| } else if (!isConfigured) {
| |
| item.append(
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge is-warning'
| |
| )
| |
| .text(
| |
| isUsed
| |
| ? 'NOT CONFIGURED'
| |
| : 'DELETED'
| |
| )
| |
| );
| |
| } else if (!isUsed) {
| |
| item.append(
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge is-warning'
| |
| )
| |
| .text('UNUSED')
| |
| );
| |
| }
| |
| | |
| list.append(item);
| |
| }
| |
| );
| |
| | |
| container.append(list);
| |
| | |
| if (!ordered.length) {
| |
| container.append(
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-empty'
| |
| )
| |
| .text(
| |
| 'No variables found in the preload.'
| |
| )
| |
| );
| |
| }
| |
| },
| |
| | |
| renderVariablesBySelector() {
| |
| const container =
| |
| this.root &&
| |
| this.root.find(
| |
| '.preload-editor-variables'
| |
| );
| |
| | |
| if (
| |
| container &&
| |
| container.length
| |
| ) {
| |
| this.renderVariables(container);
| |
| }
| |
| },
| |
| | |
| selectVariable(name) {
| |
| name =
| |
| String(name || '')
| |
| .trim()
| |
| .toUpperCase();
| |
| | |
| if (!name) {
| |
| return;
| |
| }
| |
| | |
| const available =
| |
| ['PAGE'].concat(
| |
| (
| |
| this.analysis &&
| |
| this.analysis.variables
| |
| ? this.analysis.variables
| |
| : []
| |
| ).filter(
| |
| variable =>
| |
| variable !== 'PAGE'
| |
| )
| |
| );
| |
| | |
| const configuredUnused =
| |
| this.state.config &&
| |
| this.state.config.fields
| |
| ? Object.keys(
| |
| this.state.config.fields
| |
| )
| |
| .map(
| |
| variable =>
| |
| variable.toUpperCase()
| |
| )
| |
| : [];
| |
| | |
| configuredUnused.forEach(
| |
| variable => {
| |
| if (
| |
| !available.includes(variable) &&
| |
| variable !== 'PAGE'
| |
| ) {
| |
| available.push(variable);
| |
| }
| |
| }
| |
| );
| |
| | |
| if (!available.includes(name)) {
| |
| return;
| |
| }
| |
| | |
| this.state.selectedVariable =
| |
| name;
| |
| | |
| this.renderVariablesBySelector();
| |
| this.renderVariableConfigurationBySelector();
| |
| },
| |
| | |
| renderVariableConfigurationBySelector() {
| |
| const container =
| |
| this.root &&
| |
| this.root.find(
| |
| '.preload-editor-variable-configuration'
| |
| );
| |
| | |
| if (
| |
| container &&
| |
| container.length
| |
| ) {
| |
| this.renderVariableConfiguration(
| |
| container
| |
| );
| |
| }
| |
| },
| |
| | |
| renderVariableConfiguration(container) {
| |
| container.empty();
| |
| | |
| const analysis =
| |
| this.analysis ||
| |
| this.analyzePreload(
| |
| this.extractIncludeonly(
| |
| this.getEditorContent()
| |
| )
| |
| );
| |
| | |
| const name =
| |
| this.state.selectedVariable ||
| |
| 'PAGE';
| |
| | |
| const fields =
| |
| this.state.config &&
| |
| this.state.config.fields
| |
| ? this.state.config.fields
| |
| : {};
| |
| | |
| const field =
| |
| fields[name];
| |
| | |
| if (name === 'PAGE') {
| |
| container.append(
| |
| this.createAutomaticPageCard(
| |
| field ||
| |
| this.getAutomaticPageField()
| |
| )
| |
| );
| |
| | |
| return;
| |
| }
| |
| | |
| if (!field) {
| |
| const missing =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable preload-editor-variable-missing'
| |
| );
| |
| | |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-header'
| |
| )
| |
| .append(
| |
| $('<h3>')
| |
| .text(name),
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge is-warning'
| |
| )
| |
| .text(
| |
| 'NOT CONFIGURED'
| |
| )
| |
| )
| |
| .appendTo(missing);
| |
| | |
| $('<p>')
| |
| .addClass(
| |
| 'preload-editor-analysis-note'
| |
| )
| |
| .text(
| |
| 'This variable is currently not present in config.json.'
| |
| )
| |
| .appendTo(missing);
| |
| | |
| container.append(missing);
| |
| return;
| |
| }
| |
| | |
| container.append(
| |
| this.createVariableCard(
| |
| name,
| |
| field,
| |
| analysis
| |
| )
| |
| );
| |
| },
| |
| | |
| createAutomaticPageCard(field) {
| |
| const card =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable preload-editor-variable-automatic'
| |
| )
| |
| .attr(
| |
| 'data-variable',
| |
| 'PAGE'
| |
| );
| |
| | |
| const header =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-header'
| |
| );
| |
| | |
| header.append(
| |
| $('<h3>').text('PAGE'),
| |
| | |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge'
| |
| )
| |
| .text('AUTOMATIC')
| |
| );
| |
| | |
| card.append(header);
| |
| | |
| $('<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(card);
| |
| | |
| const label =
| |
| this.textInput(
| |
| field.label ||
| |
| 'Page Name'
| |
| ).prop(
| |
| 'disabled',
| |
| true
| |
| );
| |
| | |
| const description =
| |
| this.textarea(
| |
| field.description ||
| |
| 'Name of the page.'
| |
| )
| |
| .addClass(
| |
| 'preload-editor-small-textarea'
| |
| )
| |
| .prop(
| |
| 'disabled',
| |
| true
| |
| );
| |
| | |
| const type =
| |
| this.textInput(
| |
| field.type || 'page'
| |
| ).prop(
| |
| 'disabled',
| |
| true
| |
| );
| |
| | |
| const required =
| |
| this.checkbox(
| |
| field.required !== false
| |
| ).prop(
| |
| 'disabled',
| |
| true
| |
| );
| |
| | |
| const namespace =
| |
| this.textInput(
| |
| field.namespace || 'Main'
| |
| ).prop(
| |
| 'disabled',
| |
| true
| |
| );
| |
| | |
| const allowNew =
| |
| this.checkbox(
| |
| field['allow-new'] === true
| |
| ).prop(
| |
| 'disabled',
| |
| true
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Label',
| |
| label
| |
| ),
| |
| | |
| this.fieldRow(
| |
| 'Description',
| |
| description
| |
| ),
| |
| | |
| this.fieldRow(
| |
| 'Type',
| |
| type
| |
| ),
| |
| | |
| this.fieldRow(
| |
| 'Required',
| |
| required
| |
| ),
| |
| | |
| this.fieldRow(
| |
| 'Namespace',
| |
| namespace
| |
| ),
| |
| | |
| this.fieldRow(
| |
| 'Allow new page',
| |
| allowNew
| |
| )
| |
| );
| |
| | |
| return card;
| |
| },
| |
| | |
| createVariableCard(
| |
| name,
| |
| field,
| |
| analysis
| |
| ) {
| |
| const card =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable'
| |
| )
| |
| .attr(
| |
| 'data-variable',
| |
| name
| |
| );
| |
| | |
| const header =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-header'
| |
| );
| |
| | |
| header.append(
| |
| $('<h3>').text(name)
| |
| );
| |
| | |
| if (
| |
| !analysis.variables.includes(name)
| |
| ) {
| |
| header.append(
| |
| $('<span>')
| |
| .addClass(
| |
| 'preload-editor-badge is-warning'
| |
| )
| |
| .text('UNUSED')
| |
| );
| |
| }
| |
| | |
| card.append(header);
| |
| | |
| const label =
| |
| this.textInput(
| |
| field.label || ''
| |
| );
| |
| | |
| label.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| field.label =
| |
| label.val();
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Label',
| |
| label
| |
| )
| |
| );
| |
| | |
| const description =
| |
| this.textarea(
| |
| field.description || ''
| |
| ).addClass(
| |
| 'preload-editor-small-textarea'
| |
| );
| |
| | |
| description.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| field.description =
| |
| description.val();
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Description',
| |
| description
| |
| )
| |
| );
| |
| | |
| const types = [
| |
| ['text', 'Text'],
| |
| ['textarea', 'Textarea'],
| |
| ['number', 'Number'],
| |
| ['boolean', 'Boolean'],
| |
| ['select', 'Select'],
| |
| ['multiselect', 'Multi-select'],
| |
| ['page', 'Page'],
| |
| ['user', 'User'],
| |
| ['date', 'Date']
| |
| ];
| |
| | |
| const typeSelect =
| |
| this.select(
| |
| types.map(
| |
| ([value, label]) => ({
| |
| value,
| |
| label
| |
| })
| |
| ),
| |
| field.type || 'text'
| |
| );
| |
| | |
| typeSelect.on(
| |
| 'change.preloadEditor',
| |
| () => {
| |
| field.type =
| |
| typeSelect.val();
| |
| | |
| this.markDirty();
| |
| | |
| this.renderVariableConfigurationBySelector();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Type',
| |
| typeSelect
| |
| )
| |
| );
| |
| | |
| const required =
| |
| this.checkbox(
| |
| field.required === true
| |
| );
| |
| | |
| required.on(
| |
| 'change.preloadEditor',
| |
| () => {
| |
| field.required =
| |
| required.prop('checked');
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Required',
| |
| required
| |
| )
| |
| );
| |
| | |
| const placeholder =
| |
| this.textInput(
| |
| field.placeholder || ''
| |
| );
| |
| | |
| placeholder.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const value =
| |
| placeholder.val();
| |
| | |
| if (value) {
| |
| field.placeholder =
| |
| value;
| |
| } else {
| |
| delete field.placeholder;
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Placeholder',
| |
| placeholder
| |
| )
| |
| );
| |
| | |
| const defaultInput =
| |
| this.textInput(
| |
| field.default === undefined
| |
| ? ''
| |
| : field.default
| |
| );
| |
| | |
| defaultInput.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const value =
| |
| defaultInput.val();
| |
| | |
| if (value) {
| |
| field.default =
| |
| value;
| |
| } else {
| |
| delete field.default;
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Default',
| |
| defaultInput,
| |
| 'For text-like fields this is used as a placeholder. For select fields it is the empty/default option.'
| |
| )
| |
| );
| |
| | |
| if (
| |
| field.type === 'textarea'
| |
| ) {
| |
| const maxRows =
| |
| this.textInput(
| |
| field.maxRows || ''
| |
| ).attr(
| |
| 'type',
| |
| 'number'
| |
| );
| |
| | |
| maxRows.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const value =
| |
| parseInt(
| |
| maxRows.val(),
| |
| 10
| |
| );
| |
| | |
| if (
| |
| Number.isFinite(value) &&
| |
| value > 0
| |
| ) {
| |
| field.maxRows =
| |
| value;
| |
| } else {
| |
| delete field.maxRows;
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Maximum rows',
| |
| maxRows
| |
| )
| |
| );
| |
| }
| |
| | |
| if (
| |
| field.type === 'select' ||
| |
| field.type === 'multiselect'
| |
| ) {
| |
| this.renderSelectConfiguration(
| |
| card,
| |
| field
| |
| );
| |
| }
| |
| | |
| const deleteButton =
| |
| this.createButton(
| |
| 'Delete variable',
| |
| 'preload-editor-button-danger preload-editor-delete-variable',
| |
| async () => {
| |
| const confirmed =
| |
| await this.createConfirmDialog(
| |
| 'Delete variable',
| |
| 'Delete "' +
| |
| name +
| |
| '" from the configuration?\n\nThe variable will remain in the preload, but its config.json field will be removed.',
| |
| 'Delete variable',
| |
| 'Cancel'
| |
| );
| |
| | |
| if (!confirmed) {
| |
| return;
| |
| }
| |
| | |
| this.deleteVariable(name);
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-variable-delete'
| |
| )
| |
| .append(deleteButton)
| |
| );
| |
| | |
| return card;
| |
| },
| |
| | |
| async deleteVariable(name) {
| |
| name =
| |
| String(name || '')
| |
| .trim()
| |
| .toUpperCase();
| |
| | |
| if (
| |
| !name ||
| |
| name === 'PAGE'
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| if (
| |
| !this.state.config ||
| |
| !this.state.config.fields
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| delete this.state.config.fields[name];
| |
| | |
| if (
| |
| !Array.isArray(
| |
| this.state.deletedVariables
| |
| )
| |
| ) {
| |
| this.state.deletedVariables = [];
| |
| }
| |
| | |
| if (
| |
| !this.state.deletedVariables.includes(
| |
| name
| |
| )
| |
| ) {
| |
| this.state.deletedVariables.push(
| |
| name
| |
| );
| |
| }
| |
| | |
| this.state.selectedVariable =
| |
| name;
| |
| | |
| this.markDirty();
| |
| | |
| this.renderVariablesBySelector();
| |
| this.renderVariableConfigurationBySelector();
| |
| this.renderAnalysisBySelector();
| |
| this.renderConfigReviewAvailability();
| |
| | |
| this.setStatus(
| |
| 'Variable "' +
| |
| name +
| |
| '" removed from the configuration.',
| |
| 'warning'
| |
| );
| |
| },
| |
| | |
| // =========================================================
| |
| // Select configuration
| |
| // =========================================================
| |
| | |
| renderSelectConfiguration(
| |
| card,
| |
| field
| |
| ) {
| |
| const source =
| |
| this.textInput(
| |
| field.source || '',
| |
| 'category:Example'
| |
| );
| |
| | |
| source.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const value =
| |
| source.val();
| |
| | |
| if (value) {
| |
| field.source = value;
| |
| } else {
| |
| delete field.source;
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Source',
| |
| source,
| |
| 'Currently supported source format: category:Category Name'
| |
| )
| |
| );
| |
| | |
| const sourcePrefix =
| |
| this.textInput(
| |
| field['source-prefix'] || ''
| |
| );
| |
| | |
| sourcePrefix.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const value =
| |
| sourcePrefix.val();
| |
| | |
| if (value) {
| |
| field['source-prefix'] =
| |
| value;
| |
| } else {
| |
| delete field['source-prefix'];
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Source prefix',
| |
| sourcePrefix,
| |
| 'Only source values beginning with this text are included.'
| |
| )
| |
| );
| |
| | |
| const excludeCategory =
| |
| this.textInput(
| |
| field.exclude &&
| |
| field.exclude.category ||
| |
| ''
| |
| );
| |
| | |
| excludeCategory.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const value =
| |
| excludeCategory.val();
| |
| | |
| if (value) {
| |
| field.exclude =
| |
| Object.assign(
| |
| {},
| |
| field.exclude || {},
| |
| {
| |
| category: value
| |
| }
| |
| );
| |
| } else if (
| |
| field.exclude
| |
| ) {
| |
| delete field.exclude.category;
| |
| | |
| if (
| |
| !Object.keys(
| |
| field.exclude
| |
| ).length
| |
| ) {
| |
| delete field.exclude;
| |
| }
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Exclude category',
| |
| excludeCategory
| |
| )
| |
| );
| |
| | |
| card.append(
| |
| this.createTransformControls(
| |
| field,
| |
| 'value',
| |
| 'Value transformation'
| |
| ),
| |
| | |
| this.createTransformControls(
| |
| field,
| |
| 'label',
| |
| 'Label transformation'
| |
| )
| |
| );
| |
| | |
| const optionsValue =
| |
| Array.isArray(field.options)
| |
| ? field.options
| |
| .map(
| |
| option => {
| |
| if (
| |
| typeof option ===
| |
| 'string'
| |
| ) {
| |
| return option;
| |
| }
| |
| | |
| return (
| |
| String(
| |
| option.value || ''
| |
| ) +
| |
| ' | ' +
| |
| String(
| |
| option.label ||
| |
| option.value ||
| |
| ''
| |
| )
| |
| );
| |
| }
| |
| )
| |
| .join('\n')
| |
| : '';
| |
| | |
| const options =
| |
| this.textarea(
| |
| optionsValue
| |
| ).addClass(
| |
| 'preload-editor-small-textarea'
| |
| );
| |
| | |
| options.on(
| |
| 'input.preloadEditor',
| |
| () => {
| |
| const lines =
| |
| options
| |
| .val()
| |
| .split('\n')
| |
| .map(
| |
| line =>
| |
| line.trim()
| |
| )
| |
| .filter(Boolean);
| |
| | |
| if (!lines.length) {
| |
| delete field.options;
| |
| } else {
| |
| field.options =
| |
| lines.map(
| |
| line => {
| |
| const parts =
| |
| line.split(
| |
| /\s*\|\s*/
| |
| );
| |
| | |
| if (
| |
| parts.length ===
| |
| 1
| |
| ) {
| |
| return parts[0];
| |
| }
| |
| | |
| return {
| |
| value:
| |
| parts[0],
| |
| | |
| label:
| |
| parts
| |
| .slice(1)
| |
| .join(
| |
| ' | '
| |
| )
| |
| };
| |
| }
| |
| );
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| }
| |
| );
| |
| | |
| card.append(
| |
| this.fieldRow(
| |
| 'Manual options',
| |
| options,
| |
| 'One per line. Optional label format: value | label. Leave empty when using a source.'
| |
| )
| |
| );
| |
| },
| |
| | |
| createTransformControls(
| |
| field,
| |
| property,
| |
| title
| |
| ) {
| |
| const wrapper =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-transform'
| |
| );
| |
| | |
| $('<h4>')
| |
| .text(title)
| |
| .appendTo(wrapper);
| |
| | |
| const existing =
| |
| field[property] &&
| |
| typeof field[property] === 'object'
| |
| ? field[property]
| |
| : {};
| |
| | |
| const transform =
| |
| this.select(
| |
| [
| |
| {
| |
| value: '',
| |
| label:
| |
| 'No transformation'
| |
| },
| |
| {
| |
| value:
| |
| 'remove-prefix',
| |
| label:
| |
| 'Remove prefix'
| |
| },
| |
| {
| |
| value:
| |
| 'remove-suffix',
| |
| label:
| |
| 'Remove suffix'
| |
| },
| |
| {
| |
| value: 'replace',
| |
| label:
| |
| 'Replace'
| |
| }
| |
| ],
| |
| existing.transform || ''
| |
| );
| |
| | |
| const prefix =
| |
| this.textInput(
| |
| existing.prefix || ''
| |
| );
| |
| | |
| const suffix =
| |
| this.textInput(
| |
| existing.suffix || ''
| |
| );
| |
| | |
| const from =
| |
| this.textInput(
| |
| existing.from || ''
| |
| );
| |
| | |
| const to =
| |
| this.textInput(
| |
| existing.to || ''
| |
| );
| |
| | |
| const controls =
| |
| $('<div>')
| |
| .addClass(
| |
| 'preload-editor-transform-controls'
| |
| );
| |
| | |
| const prefixRow =
| |
| this.fieldRow(
| |
| 'Prefix',
| |
| prefix
| |
| );
| |
| | |
| const suffixRow =
| |
| this.fieldRow(
| |
| 'Suffix',
| |
| suffix
| |
| );
| |
| | |
| const fromRow =
| |
| this.fieldRow(
| |
| 'From',
| |
| from
| |
| );
| |
| | |
| const toRow =
| |
| this.fieldRow(
| |
| 'To',
| |
| to
| |
| );
| |
| | |
| const renderInputs =
| |
| () => {
| |
| const type =
| |
| transform.val();
| |
| | |
| prefixRow.toggle(
| |
| type === 'remove-prefix'
| |
| );
| |
| | |
| suffixRow.toggle(
| |
| type === 'remove-suffix'
| |
| );
| |
| | |
| fromRow.toggle(
| |
| type === 'replace'
| |
| );
| |
| | |
| toRow.toggle(
| |
| type === 'replace'
| |
| );
| |
| };
| |
| | |
| const update =
| |
| () => {
| |
| const type =
| |
| transform.val();
| |
| | |
| let object = null;
| |
| | |
| if (
| |
| type === 'remove-prefix' &&
| |
| prefix.val()
| |
| ) {
| |
| object = {
| |
| transform: type,
| |
| prefix:
| |
| prefix.val()
| |
| };
| |
| } else if (
| |
| type === 'remove-suffix' &&
| |
| suffix.val()
| |
| ) {
| |
| object = {
| |
| transform: type,
| |
| suffix:
| |
| suffix.val()
| |
| };
| |
| } else if (
| |
| type === 'replace' &&
| |
| from.val()
| |
| ) {
| |
| object = {
| |
| transform: type,
| |
| from:
| |
| from.val(),
| |
| to:
| |
| to.val()
| |
| };
| |
| }
| |
| | |
| if (object) {
| |
| field[property] = object;
| |
| } else {
| |
| delete field[property];
| |
| }
| |
| | |
| this.markDirty();
| |
| this.renderConfigReviewAvailability();
| |
| };
| |
| | |
| transform.on(
| |
| 'change.preloadEditor',
| |
| () => {
| |
| update();
| |
| renderInputs();
| |
| }
| |
| );
| |
| | |
| [
| |
| prefix,
| |
| suffix,
| |
| from,
| |
| to
| |
| ].forEach(
| |
| input => {
| |
| input.on(
| |
| 'input.preloadEditor',
| |
| update
| |
| );
| |
| }
| |
| );
| |
| | |
| controls.append(
| |
| this.fieldRow(
| |
| 'Transformation',
| |
| transform
| |
| ),
| |
| | |
| prefixRow,
| |
| suffixRow,
| |
| fromRow,
| |
| toRow
| |
| );
| |
| | |
| renderInputs();
| |
| | |
| wrapper.append(controls);
| |
| | |
| return wrapper;
| |
| },
| |
| | |
| // =========================================================
| |
| // Config output
| |
| // =========================================================
| |
| | |
| getCleanConfig() {
| |
| const config =
| |
| JSON.parse(
| |
| JSON.stringify(
| |
| this.state.config
| |
| )
| |
| );
| |
| | |
| const fields =
| |
| config.fields || {};
| |
| | |
| const orderedFields = {};
| |
| | |
| orderedFields.PAGE =
| |
| this.getAutomaticPageField();
| |
| | |
| Object.keys(fields)
| |
| .filter(
| |
| name =>
| |
| name.toUpperCase() !== 'PAGE'
| |
| )
| |
| .forEach(
| |
| name => {
| |
| orderedFields[name] =
| |
| fields[name];
| |
| }
| |
| );
| |
| | |
| config.fields =
| |
| orderedFields;
| |
| | |
| return config;
| |
| },
| |
| | |
| getConfigJSON() {
| |
| return JSON.stringify(
| |
| this.getCleanConfig(),
| |
| null,
| |
| 4
| |
| );
| |
| },
| |
| | |
| renderConfigPreview(container) {
| |
| if (container) {
| |
| container.text(
| |
| this.getConfigJSON()
| |
| );
| |
| }
| |
| },
| |
| | |
| renderConfigPreviewBySelector() {
| |
| this.renderAnalysisBySelector();
| |
| },
| |
| | |
| renderConfigReviewAvailability() {
| |
| this.renderAnalysisBySelector();
| |
| },
| |
| | |
| // =========================================================
| |
| // Dirty state
| |
| // =========================================================
| |
| | |
| updateDirtyState() {
| |
| this.state.dirty =
| |
| !!(
| |
| this.state.dirtyPreload ||
| |
| this.state.dirtyConfig
| |
| );
| |
| },
| |
| | |
| markPreloadDirty() {
| |
| this.state.dirtyPreload = true;
| |
| this.updateDirtyState();
| |
| this.saveDraft();
| |
| },
| |
| | |
| markConfigDirty() {
| |
| this.state.dirtyConfig = true;
| |
| this.updateDirtyState();
| |
| this.saveDraft();
| |
| },
| |
| | |
| markDirty() {
| |
| this.state.dirtyPreload = true;
| |
| this.state.dirtyConfig = true;
| |
| this.updateDirtyState();
| |
| this.saveDraft();
| |
| },
| |
| | |
| // =========================================================
| |
| // Save
| |
| // =========================================================
| |
| | |
| async savePreloadOnly() {
| |
| let title =
| |
| String(
| |
| this.state.preloadTitle || ''
| |
| ).trim();
| |
| | |
| if (!title) {
| |
| const result =
| |
| await this.createPromptDialog(
| |
| 'Preload title',
| |
| 'Enter the full title for the preload.'
| |
| );
| |
| | |
| if (!result) {
| |
| return;
| |
| }
| |
| | |
| title = result;
| |
| this.state.preloadTitle =
| |
| title;
| |
| }
| |
| | |
| this.state.preloadContent =
| |
| this.getEditorContent();
| |
| | |
| const validation =
| |
| this.validateBeforeSave();
| |
| | |
| if (validation.errors.length) {
| |
| this.showValidationErrors(
| |
| validation
| |
| );
| |
| | |
| this.saveDraft();
| |
| return;
| |
| }
| |
| | |
| const confirmed =
| |
| await this.confirmSave(
| |
| 'preload',
| |
| title
| |
| );
| |
| | |
| if (!confirmed) {
| |
| return;
| |
| }
| |
| | |
| this.setStatus(
| |
| 'Saving preload...',
| |
| 'loading'
| |
| );
| |
| | |
| try {
| |
| const exists =
| |
| await this.pageExists(title);
| |
| | |
| await this.savePage(
| |
| title,
| |
| this.state.preloadContent,
| |
| exists
| |
| ? 'Updated preload with Preload Editor'
| |
| : 'Created preload with Preload Editor',
| |
| !exists
| |
| );
| |
| | |
| this.state.mode = 'edit';
| |
| this.state.preloadTitle = title;
| |
| this.state.dirtyPreload = false;
| |
| | |
| this.updateDirtyState();
| |
| | |
| if (!this.state.dirty) {
| |
| this.clearDraft(title);
| |
| } else {
| |
| this.saveDraft();
| |
| }
| |
| | |
| this.setStatus(
| |
| this.state.dirtyConfig
| |
| ? 'Preload saved. Config still has unsaved changes.'
| |
| : 'Preload saved successfully.',
| |
| 'success'
| |
| );
| |
| | |
| mw.notify(
| |
| this.state.dirtyConfig
| |
| ? 'Preload saved. Config still has unsaved changes.'
| |
| : 'Preload saved.',
| |
| {
| |
| type: 'success'
| |
| }
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor save:',
| |
| error
| |
| );
| |
| | |
| this.saveDraft();
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not save preload.',
| |
| 'error'
| |
| );
| |
| | |
| mw.notify(
| |
| error.message ||
| |
| 'Could not save preload.',
| |
| {
| |
| type: 'error'
| |
| }
| |
| );
| |
| }
| |
| },
| |
| | |
| async saveConfigOnly() {
| |
| let title =
| |
| String(
| |
| this.state.preloadTitle || ''
| |
| ).trim();
| |
| | |
| if (!title) {
| |
| const result =
| |
| await this.createPromptDialog(
| |
| 'Preload title',
| |
| 'Enter the full title for the preload.'
| |
| );
| |
| | |
| if (!result) {
| |
| return;
| |
| }
| |
| | |
| title = result;
| |
| this.state.preloadTitle =
| |
| title;
| |
| }
| |
| | |
| this.state.preloadContent =
| |
| this.getEditorContent();
| |
| | |
| this.reanalyze();
| |
| | |
| const validation =
| |
| this.validateBeforeSave();
| |
| | |
| if (validation.errors.length) {
| |
| this.showValidationErrors(
| |
| validation
| |
| );
| |
| | |
| this.saveDraft();
| |
| return;
| |
| }
| |
| | |
| const confirmed =
| |
| await this.confirmSave(
| |
| 'config',
| |
| title
| |
| );
| |
| | |
| if (!confirmed) {
| |
| return;
| |
| }
| |
| | |
| const configTitle =
| |
| title.replace(
| |
| /\/+$/,
| |
| ''
| |
| ) +
| |
| '/config.json';
| |
| | |
| this.setStatus(
| |
| 'Saving config...',
| |
| 'loading'
| |
| );
| |
| | |
| try {
| |
| /*
| |
| * This now automatically creates or converts
| |
| * config.json to the JSON content model.
| |
| */
| |
| await this.saveConfigPage(
| |
| configTitle,
| |
| this.getConfigJSON(),
| |
| (
| |
| await this.pageExists(
| |
| configTitle
| |
| )
| |
| )
| |
| ? 'Updated preload config with Preload Editor'
| |
| : 'Created preload config with Preload Editor'
| |
| );
| |
| | |
| this.state.mode = 'edit';
| |
| this.state.preloadTitle = title;
| |
| this.state.dirtyConfig = false;
| |
| | |
| this.updateDirtyState();
| |
| | |
| if (!this.state.dirty) {
| |
| this.clearDraft(title);
| |
| } else {
| |
| this.saveDraft();
| |
| }
| |
| | |
| this.setStatus(
| |
| this.state.dirtyPreload
| |
| ? 'Config saved as JSON. Preload still has unsaved changes.'
| |
| : 'Config saved as JSON successfully.',
| |
| 'success'
| |
| );
| |
| | |
| mw.notify(
| |
| this.state.dirtyPreload
| |
| ? 'Config saved as JSON. Preload still has unsaved changes.'
| |
| : 'Config saved as JSON.',
| |
| {
| |
| type: 'success'
| |
| }
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor config save:',
| |
| error
| |
| );
| |
| | |
| this.saveDraft();
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not save config.',
| |
| 'error'
| |
| );
| |
| | |
| mw.notify(
| |
| error.message ||
| |
| 'Could not save config.',
| |
| {
| |
| type: 'error'
| |
| }
| |
| );
| |
| }
| |
| },
| |
| | |
| async saveAll() {
| |
| let title =
| |
| String(
| |
| this.state.preloadTitle || ''
| |
| ).trim();
| |
| | |
| if (!title) {
| |
| const result =
| |
| await this.createPromptDialog(
| |
| 'Preload title',
| |
| 'Enter the full title for the preload.'
| |
| );
| |
| | |
| if (!result) {
| |
| return;
| |
| }
| |
| | |
| title = result;
| |
| this.state.preloadTitle =
| |
| title;
| |
| }
| |
| | |
| this.state.preloadContent =
| |
| this.getEditorContent();
| |
| | |
| this.reanalyze();
| |
| | |
| const validation =
| |
| this.validateBeforeSave();
| |
| | |
| if (validation.errors.length) {
| |
| this.showValidationErrors(
| |
| validation
| |
| );
| |
| | |
| this.saveDraft();
| |
| return;
| |
| }
| |
| | |
| const confirmed =
| |
| await this.confirmSave(
| |
| 'both',
| |
| title
| |
| );
| |
| | |
| if (!confirmed) {
| |
| return;
| |
| }
| |
| | |
| const configTitle =
| |
| title.replace(
| |
| /\/+$/,
| |
| ''
| |
| ) +
| |
| '/config.json';
| |
| | |
| this.setStatus(
| |
| 'Saving preload and config...',
| |
| 'loading'
| |
| );
| |
| | |
| try {
| |
| const preloadExists =
| |
| await this.pageExists(title);
| |
| | |
| await this.savePage(
| |
| title,
| |
| this.state.preloadContent,
| |
| preloadExists
| |
| ? 'Updated preload with Preload Editor'
| |
| : 'Created preload with Preload Editor',
| |
| !preloadExists
| |
| );
| |
| | |
| /*
| |
| * Config is saved through the JSON-aware
| |
| * helper. This means:
| |
| *
| |
| * - new config -> created as JSON
| |
| * - old wikitext config -> converted to JSON
| |
| * - existing JSON config -> simply updated
| |
| */
| |
| const configExists =
| |
| await this.pageExists(
| |
| configTitle
| |
| );
| |
| | |
| await this.saveConfigPage(
| |
| configTitle,
| |
| this.getConfigJSON(),
| |
| configExists
| |
| ? 'Updated preload config with Preload Editor'
| |
| : 'Created preload config with Preload Editor'
| |
| );
| |
| | |
| this.state.mode = 'edit';
| |
| this.state.dirtyPreload = false;
| |
| this.state.dirtyConfig = false;
| |
| this.state.dirty = false;
| |
| this.state.preloadTitle = title;
| |
| | |
| this.clearDraft(title);
| |
| | |
| this.setStatus(
| |
| 'Preload and JSON config saved successfully.',
| |
| 'success'
| |
| );
| |
| | |
| mw.notify(
| |
| 'Preload and config saved. config.json is using the JSON content model.',
| |
| {
| |
| type: 'success'
| |
| }
| |
| );
| |
| } catch (error) {
| |
| console.error(
| |
| 'Preload Editor save:',
| |
| error
| |
| );
| |
| | |
| this.saveDraft();
| |
| | |
| this.setStatus(
| |
| error.message ||
| |
| 'Could not save preload/config.',
| |
| 'error'
| |
| );
| |
| | |
| mw.notify(
| |
| error.message ||
| |
| 'Could not save preload/config.',
| |
| {
| |
| type: 'error'
| |
| }
| |
| );
| |
| }
| |
| },
| |
| | |
| // =========================================================
| |
| // Validation
| |
| // =========================================================
| |
| | |
| validateBeforeSave() {
| |
| const errors = [];
| |
| const warnings = [];
| |
| | |
| const title =
| |
| String(
| |
| this.state.preloadTitle || ''
| |
| ).trim();
| |
| | |
| if (!title) {
| |
| errors.push(
| |
| 'A preload title is required.'
| |
| );
| |
| }
| |
| | |
| if (
| |
| title &&
| |
| !/\/Preload$/i.test(title)
| |
| ) {
| |
| warnings.push(
| |
| 'The title does not end with "/Preload".'
| |
| );
| |
| }
| |
| | |
| const content =
| |
| this.getEditorContent();
| |
| | |
| this.state.preloadContent =
| |
| content;
| |
| | |
| if (!content.trim()) {
| |
| errors.push(
| |
| 'The preload cannot be empty.'
| |
| );
| |
| }
| |
| | |
| if (
| |
| !/<includeonly>/i.test(content)
| |
| ) {
| |
| warnings.push(
| |
| 'The preload does not contain an <includeonly> section.'
| |
| );
| |
| }
| |
| | |
| if (
| |
| !/<noinclude>/i.test(content)
| |
| ) {
| |
| warnings.push(
| |
| 'The preload does not contain a <noinclude> section.'
| |
| );
| |
| }
| |
| | |
| const analysis =
| |
| this.analyzePreload(
| |
| this.extractIncludeonly(
| |
| content
| |
| )
| |
| );
| |
| | |
| this.state.config =
| |
| this.prepareConfigForVariables(
| |
| this.state.config ||
| |
| this.emptyConfig(),
| |
| analysis
| |
| );
| |
| | |
| const configValidation =
| |
| this.validateConfig(
| |
| this.state.config,
| |
| analysis
| |
| );
| |
| | |
| errors.push(
| |
| ...configValidation.errors
| |
| );
| |
| | |
| warnings.push(
| |
| ...configValidation.warnings
| |
| );
| |
| | |
| let depth = 0;
| |
| | |
| analysis.conditions.forEach(
| |
| condition => {
| |
| if (
| |
| condition.type === 'if'
| |
| ) {
| |
| depth++;
| |
| } else if (
| |
| condition.type === 'endif'
| |
| ) {
| |
| depth--;
| |
| | |
| if (depth < 0) {
| |
| errors.push(
| |
| 'Found @endif without a matching @if.'
| |
| );
| |
| | |
| depth = 0;
| |
| }
| |
| } else if (
| |
| condition.type === 'elseif' ||
| |
| condition.type === 'else'
| |
| ) {
| |
| if (depth === 0) {
| |
| errors.push(
| |
| '@' +
| |
| condition.type +
| |
| ' exists outside an @if block.'
| |
| );
| |
| }
| |
| }
| |
| }
| |
| );
| |
| | |
| if (depth > 0) {
| |
| errors.push(
| |
| 'One or more @if blocks are missing @endif.'
| |
| );
| |
| }
| |
| | |
| return {
| |
| errors,
| |
| warnings,
| |
| valid:
| |
| errors.length === 0
| |
| };
| |
| },
| |
| | |
| showValidationErrors(validation) {
| |
| this.renderAnalysisBySelector();
| |
| | |
| this.saveDraft();
| |
| | |
| this.setStatus(
| |
| validation.errors.join(' '),
| |
| 'error'
| |
| );
| |
| | |
| mw.notify(
| |
| validation.errors.join('\n'),
| |
| {
| |
| type: 'error'
| |
| }
| |
| );
| |
| }
| |
| };
| |
| | |
| mw.PreloadEditor =
| |
| PreloadEditor;
| |
| | |
| $(function () {
| |
| if (
| |
| mw.config.get('wgPageName') !==
| |
| 'Petit_Planet:Preload_Editor'
| |
| ) {
| |
| return;
| |
| }
| |
| | |
| PreloadEditor.init();
| |
| });
| |
| | |
| }(mediaWiki, jQuery)); | |