MediaWiki:Gadget-PreloadEditor.js: Difference between revisions
MediaWiki interface page
More actions
ReisuDesign (talk | contribs) No edit summary |
ReisuDesign (talk | contribs) No edit summary |
||
| Line 3: | Line 3: | ||
const PreloadEditor = { | const PreloadEditor = { | ||
version: '0. | version: '0.7.0', | ||
state: { | state: { | ||
| Line 13: | Line 13: | ||
conditions: [], | conditions: [], | ||
selectedVariable: 'PAGE', | selectedVariable: 'PAGE', | ||
deletedVariables: [], | |||
mode: 'idle', | mode: 'idle', | ||
dirty: false | dirty: false | ||
| Line 25: | Line 26: | ||
editorTools: null, | editorTools: null, | ||
draftStorageKey: | draftStorageKey: | ||
'PetitPlanet.PreloadEditor.drafts.v1', | 'PetitPlanet.PreloadEditor.drafts.v1', | ||
| Line 112: | Line 106: | ||
getDraftId(title) { | getDraftId(title) { | ||
title = | title = | ||
String(title || '') | String(title || '').trim(); | ||
return title.toLowerCase(); | return title | ||
? title.toLowerCase() | |||
: '__untitled__'; | |||
}, | }, | ||
| Line 130: | Line 117: | ||
this.getEditorContent(); | this.getEditorContent(); | ||
if ( | if ( | ||
!this.editorView && | !this.editorView && | ||
| Line 169: | Line 152: | ||
preloadTitle: | preloadTitle: | ||
String( | String( | ||
this.state.preloadTitle || | this.state.preloadTitle || '' | ||
), | ), | ||
preloadSource: | preloadSource: | ||
String( | String( | ||
this.state.preloadSource || | this.state.preloadSource || '' | ||
), | ), | ||
preloadContent: | preloadContent: | ||
String( | String( | ||
content || | content || '' | ||
), | ), | ||
| Line 206: | Line 186: | ||
'PAGE' | 'PAGE' | ||
), | ), | ||
deletedVariables: | |||
Array.isArray( | |||
this.state.deletedVariables | |||
) | |||
? this.state.deletedVariables.slice() | |||
: [], | |||
mode: | mode: | ||
| Line 219: | Line 206: | ||
saveDraft() { | saveDraft() { | ||
if (!this.state.dirty) { | if (!this.state.dirty) { | ||
return; | return; | ||
| Line 241: | Line 224: | ||
snapshot; | snapshot; | ||
storage.active = id; | storage.active = id; | ||
| Line 258: | Line 236: | ||
const id = | const id = | ||
this.getDraftId( | this.getDraftId(title); | ||
if ( | if ( | ||
| Line 293: | Line 269: | ||
} | } | ||
if ( | if ( | ||
storage.active && | storage.active && | ||
| Line 311: | Line 284: | ||
} | } | ||
let latestId = null; | let latestId = null; | ||
let latestDraft = null; | let latestDraft = null; | ||
| Line 323: | Line 293: | ||
if ( | if ( | ||
!draft || | !draft || | ||
typeof draft !== | typeof draft !== 'object' | ||
) { | ) { | ||
return; | return; | ||
| Line 348: | Line 317: | ||
); | ); | ||
if ( | if (!latestDraft) { | ||
return null; | return null; | ||
} | } | ||
| Line 360: | Line 327: | ||
}, | }, | ||
restoreDraft() { | async restoreDraft() { | ||
const result = | const result = | ||
this.getMostRecentDraft(); | this.getMostRecentDraft(); | ||
| Line 376: | Line 343: | ||
const title = | const title = | ||
String( | String( | ||
draft.preloadTitle || | draft.preloadTitle || '' | ||
).trim(); | ).trim(); | ||
| Line 394: | Line 360: | ||
'Preload Editor found unsaved local work for ' + | 'Preload Editor found unsaved local work for ' + | ||
displayTitle + | displayTitle + | ||
(savedAt | ( | ||
savedAt | |||
? '\n\nLast saved locally: ' + | |||
savedAt | |||
: '' | |||
) + | |||
'\n\nRestore it?'; | '\n\nRestore it?'; | ||
const restore = | const restore = | ||
await this.createConfirmDialog( | |||
message | 'Restore unsaved work', | ||
message, | |||
'Restore', | |||
'Discard' | |||
); | ); | ||
if (!restore) { | if (!restore) { | ||
this.clearDraft(title); | |||
this.clearDraft( | |||
return false; | return false; | ||
} | } | ||
| Line 423: | Line 386: | ||
if ( | if ( | ||
draft.config && | draft.config && | ||
typeof draft.config === | typeof draft.config === 'object' && | ||
!Array.isArray(draft.config) | |||
!Array.isArray( | |||
) { | ) { | ||
try { | try { | ||
| Line 438: | Line 398: | ||
) | ) | ||
); | ); | ||
} catch ( | } catch (error) { | ||
console.warn( | console.warn( | ||
'Preload Editor: Could not restore local config.', | 'Preload Editor: Could not restore local config.', | ||
| Line 454: | Line 412: | ||
preloadSource: | preloadSource: | ||
String( | String( | ||
draft.preloadSource || | draft.preloadSource || '' | ||
), | ), | ||
preloadContent: | preloadContent: | ||
String( | String( | ||
draft.preloadContent || | draft.preloadContent || '' | ||
), | ), | ||
| Line 486: | Line 442: | ||
), | ), | ||
mode: | deletedVariables: | ||
draft.mode || | Array.isArray( | ||
'edit', | draft.deletedVariables | ||
) | |||
? draft.deletedVariables | |||
.map( | |||
name => | |||
String( | |||
name | |||
).toUpperCase() | |||
) | |||
.filter( | |||
name => | |||
name !== | |||
'PAGE' | |||
) | |||
: [], | |||
mode: | |||
draft.mode || | |||
'edit', | |||
dirty: true | dirty: true | ||
}; | }; | ||
this.reanalyze(); | this.reanalyze(); | ||
this.renderEditor(); | this.renderEditor(); | ||
| Line 520: | Line 488: | ||
async getPage(title) { | async getPage(title) { | ||
const response = await new mw.Api().get({ | const response = | ||
await new mw.Api().get({ | |||
action: 'query', | |||
prop: 'revisions', | |||
titles: title, | |||
rvprop: 'content', | |||
rvslots: 'main', | |||
formatversion: 2 | |||
}); | |||
const page = | const page = | ||
| Line 535: | Line 504: | ||
response.query.pages[0]; | response.query.pages[0]; | ||
if (!page || page.missing) return null; | if (!page || page.missing) { | ||
return null; | |||
} | |||
const content = | const content = | ||
| Line 545: | Line 516: | ||
return { | return { | ||
title: page.title || title, | title: | ||
page.title || | |||
title, | |||
content: | content: | ||
typeof content === 'string' | typeof content === 'string' | ||
| Line 558: | Line 532: | ||
async searchPages(query) { | async searchPages(query) { | ||
const response = await new mw.Api().get({ | const response = | ||
await new mw.Api().get({ | |||
action: 'query', | |||
list: 'search', | |||
srsearch: query, | |||
srlimit: 10, | |||
srnamespace: 10, | |||
formatversion: 2 | |||
}); | |||
return ( | return ( | ||
| Line 621: | Line 596: | ||
while ( | while ( | ||
(match = pattern.exec(content)) !== | (match = pattern.exec(content)) !== null | ||
) { | ) { | ||
const name = | const name = | ||
| Line 643: | Line 617: | ||
while ( | while ( | ||
(match = pattern.exec(content)) !== | (match = pattern.exec(content)) !== null | ||
) { | ) { | ||
conditions.push({ | conditions.push({ | ||
type: | type: | ||
match[1].toLowerCase(), | match[1].toLowerCase(), | ||
expression: | expression: | ||
match[2].trim(), | match[2].trim(), | ||
index: match.index | |||
index: | |||
match.index | |||
}); | }); | ||
} | } | ||
| Line 664: | Line 640: | ||
condition => { | condition => { | ||
if ( | if ( | ||
condition.type !== | condition.type !== 'if' && | ||
condition.type !== 'elseif' | |||
condition.type !== | |||
) { | ) { | ||
return; | return; | ||
| Line 677: | Line 651: | ||
) || []; | ) || []; | ||
matches.forEach(name => { | matches.forEach( | ||
name => { | |||
name = | |||
name.toUpperCase(); | |||
if ( | |||
!variables.includes( | |||
name | |||
) | |||
) { | |||
variables.push( | |||
name | |||
); | |||
} | |||
} | } | ||
); | |||
} | } | ||
); | ); | ||
| Line 700: | Line 676: | ||
return { | return { | ||
variables: | variables: | ||
this.findVariables( | this.findVariables(content), | ||
conditions: | conditions: | ||
this.findConditions( | this.findConditions(content), | ||
conditionVariables: | conditionVariables: | ||
| Line 754: | Line 726: | ||
if ( | if ( | ||
!config.settings || | !config.settings || | ||
typeof config.settings !== | typeof config.settings !== 'object' || | ||
Array.isArray(config.settings) | |||
Array.isArray( | ) { | ||
) { | |||
config.settings = {}; | config.settings = {}; | ||
} | } | ||
| Line 765: | Line 734: | ||
if ( | if ( | ||
!config.fields || | !config.fields || | ||
typeof config.fields !== | typeof config.fields !== 'object' || | ||
Array.isArray(config.fields) | |||
Array.isArray( | |||
) { | ) { | ||
config.fields = {}; | config.fields = {}; | ||
} | } | ||
config.fields.PAGE = | config.fields.PAGE = | ||
this.getAutomaticPageField(); | this.getAutomaticPageField(); | ||
| Line 793: | Line 752: | ||
return { | return { | ||
label: | label: | ||
this.humanizeName( | this.humanizeName(name), | ||
description: '', | description: '', | ||
| Line 835: | Line 792: | ||
) { | ) { | ||
config = | config = | ||
this.normalizeConfig( | this.normalizeConfig(config); | ||
const existing = | const existing = | ||
config.fields || {}; | config.fields || {}; | ||
const deleted = | |||
new Set( | |||
Array.isArray( | |||
this.state.deletedVariables | |||
) | |||
? this.state.deletedVariables | |||
.map( | |||
name => | |||
String( | |||
name | |||
).toUpperCase() | |||
) | |||
: [] | |||
); | |||
const fields = {}; | const fields = {}; | ||
fields.PAGE = | fields.PAGE = | ||
this.getAutomaticPageField(); | this.getAutomaticPageField(); | ||
analysis.variables.forEach( | analysis.variables.forEach( | ||
name => { | name => { | ||
if (name === 'PAGE') { | if ( | ||
name === 'PAGE' || | |||
deleted.has(name) | |||
) { | |||
return; | return; | ||
} | } | ||
| Line 875: | Line 835: | ||
); | ); | ||
Object.keys(existing).forEach( | Object.keys(existing).forEach( | ||
name => { | name => { | ||
| Line 890: | Line 842: | ||
if ( | if ( | ||
upper !== 'PAGE' && | upper !== 'PAGE' && | ||
!fields[upper] | !fields[upper] && | ||
!deleted.has(upper) | |||
) { | ) { | ||
fields[upper] = | fields[upper] = | ||
| Line 926: | Line 879: | ||
if ( | if ( | ||
!config.fields || | !config.fields || | ||
typeof config.fields !== | typeof config.fields !== 'object' || | ||
Array.isArray(config.fields) | |||
Array.isArray( | |||
) { | ) { | ||
return { | return { | ||
| Line 952: | Line 902: | ||
name => { | name => { | ||
if ( | if ( | ||
!configured.includes( | !configured.includes(name) | ||
) { | ) { | ||
errors.push( | errors.push( | ||
| Line 982: | Line 930: | ||
if ( | if ( | ||
!configured.includes( | !configured.includes('PAGE') | ||
) { | ) { | ||
errors.push( | errors.push( | ||
| Line 997: | Line 943: | ||
if ( | if ( | ||
!field || | !field || | ||
typeof field !== | typeof field !== 'object' || | ||
Array.isArray(field) | Array.isArray(field) | ||
) { | ) { | ||
| Line 1,020: | Line 965: | ||
if ( | if ( | ||
( | ( | ||
field.type === | field.type === 'select' || | ||
field.type === 'multiselect' | |||
field.type === | |||
) && | ) && | ||
!field.source && | !field.source && | ||
!Array.isArray( | !Array.isArray(field.options) | ||
) { | ) { | ||
warnings.push( | warnings.push( | ||
| Line 1,106: | Line 1,047: | ||
if (className) { | if (className) { | ||
section.addClass( | section.addClass(className); | ||
} | } | ||
| Line 1,138: | Line 1,077: | ||
if (className) { | if (className) { | ||
button.addClass( | button.addClass(className); | ||
} | } | ||
| Line 1,258: | Line 1,195: | ||
) === | ) === | ||
String( | String( | ||
value === | value === undefined | ||
? '' | ? '' | ||
: value | : value | ||
) | ) | ||
) | ) | ||
.appendTo( | .appendTo(select); | ||
} | } | ||
); | ); | ||
| Line 1,273: | Line 1,207: | ||
}, | }, | ||
// | // 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 => { | |||
$('<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'); | |||
/* | |||
* Keep the browser from submitting or | |||
* otherwise handling Enter/Escape itself. | |||
*/ | |||
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'); | |||
} | |||
} | |||
); | |||
} | } | ||
); | |||
}, | }, | ||
// | // CodeMirror | ||
destroyEditor() { | |||
if (!this.editor) { | |||
this.editorView = null; | |||
this.editorTools = null; | |||
this. | |||
return; | return; | ||
} | } | ||
const | 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 || ''); | |||
String( | |||
if (!this.editorView) { | |||
this.state.preloadContent = | |||
content; | |||
return; | |||
} | |||
const | const current = | ||
this. | this.editorView.state.doc.toString(); | ||
if ( | if (current === content) { | ||
return; | 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 = | |||
text | 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( | this.insertEditorText( | ||
replacement, | |||
selected.from, | selected.from, | ||
selected.to, | selected.to, | ||
selected.from + | selected.from + | ||
replacement.length | |||
); | ); | ||
}, | }, | ||
insertVariable(name) { | |||
name = | |||
String(name || '') | |||
.trim() | |||
.toUpperCase(); | |||
if (!name) { | |||
return; | |||
} | |||
const selected = | |||
this.getEditorSelection(); | |||
if ( | 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) { | |||
if (! | |||
return; | return; | ||
} | } | ||
let text; | |||
let cursorOffset; | |||
switch (type) { | |||
case 'if': { | |||
if (!variable) { | |||
return; | |||
} | |||
const opening = | |||
'<!-- @if ' + | |||
variable + | |||
' -->'; | |||
const closing = | |||
'<!-- @endif -->'; | |||
if ( | 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 | |||
); | ); | ||
}, | |||
const | getToolVariables() { | ||
const variables = | |||
new Set(['PAGE']); | |||
if ( | |||
. | this.analysis && | ||
this.analysis.variables | |||
) { | |||
. | this.analysis.variables.forEach( | ||
name => { | |||
variables.add( | |||
String( | |||
name | |||
).toUpperCase() | |||
); | |||
} | |||
); | ); | ||
} | |||
const | 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 | const currentCondition = | ||
this. | this.editorTools | ||
.conditionVariableSelect | |||
.val(); | |||
const | 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; | |||
} | |||
this.editorTools | const type = | ||
this.editorTools | |||
.conditionTypeSelect | |||
.val(); | |||
this. | 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 with %%', | |||
'preload-editor-tool-button', | |||
() => | |||
this.wrapSelectionWithVariable() | |||
) | |||
); | |||
const | 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 | 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 | 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() | |||
); | |||
name | |||
} | } | ||
); | |||
} | } | ||
return | return known; | ||
}, | }, | ||
createSyntaxDecorations( | |||
state, | |||
) { | Decoration | ||
) { | |||
const decorations = []; | |||
const source = | |||
state.doc.toString(); | |||
const | const knownVariables = | ||
this.getKnownVariables(); | |||
const | 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 | const onReady = | ||
async cm => { | |||
if ( | if ( | ||
settled || | settled || | ||
!cm || | |||
cm.textarea !== textarea || | |||
!cm.view | |||
) { | ) { | ||
return; | return; | ||
} | } | ||
settled = true; | |||
cleanup(); | |||
const view = | |||
cm.view; | |||
const inputHandler = | |||
() => { | |||
this.state | |||
.preloadContent = | |||
view.state.doc.toString(); | |||
this.state.dirty = | |||
true; | |||
this.reanalyze(); | |||
this.renderVariablesBySelector(); | |||
this.renderVariableConfigurationBySelector(); | |||
this.renderAnalysisBySelector(); | |||
this.renderConfigPreviewBySelector(); | |||
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. | this.root = | ||
$('<div>') | $('<div>') | ||
.attr( | |||
'id', | |||
'preload-editor' | |||
) | |||
.addClass( | .addClass( | ||
'preload-editor | 'preload-editor' | ||
); | ); | ||
root.append(this.root); | |||
); | this.renderShell(); | ||
this. | this.restoreDraft(); | ||
this. | window.addEventListener( | ||
'beforeunload', | |||
() => { | |||
if (this.state.dirty) { | |||
this.saveDraft(); | |||
} | |||
} | |||
); | ); | ||
}, | }, | ||
renderShell() { | |||
this. | this.destroyEditor(); | ||
this.root.empty(); | |||
const | const loader = | ||
this.createSection( | 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() | |||
); | |||
this. | 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.status = | |||
$('<div>') | |||
.addClass( | |||
'preload-editor-status' | |||
); | |||
. | |||
this.root.append(this.status); | |||
this.workspace = | |||
$('<div>') | |||
.addClass( | |||
'preload-editor-workspace' | |||
); | |||
this. | this.root.append(this.workspace); | ||
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' | |||
); | |||
this. | 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 | return; | ||
} | } | ||
this.setStatus( | |||
'Loading preload...', | |||
'loading' | |||
); | |||
const | 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( | |||
this. | error.message || | ||
' | 'Could not load preload.', | ||
' | 'error' | ||
); | |||
} | |||
}, | }, | ||
async | async findSimilarPreloads(title) { | ||
let query = | |||
title | |||
.replace( | |||
/\/Preload$/i, | |||
'' | |||
) | |||
.trim(); | .trim(); | ||
if (!query) { | |||
query = title; | |||
} | |||
try { | |||
this. | const results = | ||
await this.searchPages(query); | |||
return results.filter( | |||
result => | |||
/\/Preload$/i.test( | |||
result.title | |||
) | |||
); | |||
} catch (error) { | |||
console.error( | |||
'Preload Editor search:', | |||
error | |||
); | |||
return []; | |||
} | |||
}, | |||
this. | renderMissingPage( | ||
title, | |||
suggestions | |||
) { | |||
this.workspace.empty(); | |||
const section = | |||
this.createSection( | |||
'Preload not found' | |||
const | |||
' | |||
); | ); | ||
$('<p>') | |||
.text( | |||
'No preload exists at "' + | |||
title + | |||
'".' | |||
) | |||
.appendTo(section); | |||
if (suggestions.length) { | |||
' | $('<h3>') | ||
.text('Similar preloads') | |||
.appendTo(section); | |||
const list = | |||
const | $('<div>') | ||
.addClass( | |||
'preload-editor-suggestions' | |||
); | |||
suggestions.forEach( | |||
result => { | |||
list.append( | |||
this.createButton( | |||
result.title, | |||
'preload-editor-suggestion', | |||
() => | |||
this.loadExisting( | |||
result.title | |||
) | |||
) | |||
); | |||
} | |||
); | |||
section.append(list); | |||
} | |||
this. | 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, | |||
this. | config: | ||
this.emptyConfig(), | |||
variables: ['PAGE'], | |||
conditions: [], | |||
selectedVariable: 'PAGE', | |||
deletedVariables: [], | |||
mode: 'new', | |||
dirty: true | |||
}; | |||
this.saveDraft(); | |||
this.renderEditor(); | |||
this.setStatus( | |||
'Creating a new preload.', | |||
'success' | |||
); | |||
}, | |||
this. | 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' | |||
); | |||
const | 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 | |||
}; | |||
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( | |||
/\/+$/, | |||
'' | |||
const configTitle = | |||
title.replace( | |||
/\/+$/, | |||
'' | |||
) + | ) + | ||
'/config.json'; | '/config.json'; | ||
const configPage = | const configPage = | ||
await this.getPage( | await this.getPage(configTitle); | ||
if (configPage) { | if (configPage) { | ||
| Line 3,026: | Line 2,885: | ||
configPage.content | configPage.content | ||
); | ); | ||
} catch ( | } catch (error) { | ||
throw new Error( | throw new Error( | ||
'The existing config.json contains invalid JSON: ' + | 'The existing config.json contains invalid JSON: ' + | ||
| Line 3,037: | Line 2,894: | ||
const analysis = | const analysis = | ||
this.analyzePreload( | this.analyzePreload(content); | ||
this.state.deletedVariables = []; | |||
config = | config = | ||
| Line 3,060: | Line 2,917: | ||
selectedVariable: | selectedVariable: | ||
analysis.variables.includes( | analysis.variables.includes('PAGE') | ||
? 'PAGE' | ? 'PAGE' | ||
: ( | : ( | ||
| Line 3,068: | Line 2,923: | ||
'PAGE' | 'PAGE' | ||
), | ), | ||
deletedVariables: [], | |||
mode: 'edit', | mode: 'edit', | ||
| Line 3,073: | Line 2,930: | ||
}; | }; | ||
this.clearDraft(title); | this.clearDraft(title); | ||
this.renderEditor(); | this.renderEditor(); | ||
}, | }, | ||
| Line 3,127: | Line 2,975: | ||
'New preload' | 'New preload' | ||
) | ) | ||
.appendTo( | .appendTo(meta); | ||
if ( | if ( | ||
this.state.mode === | this.state.mode === 'new' | ||
) { | ) { | ||
$('<span>') | $('<span>') | ||
| Line 3,140: | Line 2,985: | ||
) | ) | ||
.text('NEW') | .text('NEW') | ||
.appendTo( | .appendTo(meta); | ||
} | } | ||
editorSection.append( | editorSection.append(meta); | ||
editorSection.append( | editorSection.append( | ||
this.renderEditorTools() | this.renderEditorTools() | ||
| Line 3,170: | Line 3,010: | ||
'preload-editor-codemirror-mount' | 'preload-editor-codemirror-mount' | ||
) | ) | ||
.append( | .append(textarea); | ||
editorSection.append( | editorSection.append(editorMount); | ||
left.append(editorSection); | |||
left.append( | |||
const analysisSection = | const analysisSection = | ||
this.createSection( | this.createSection('Analysis'); | ||
const analysisContainer = | const analysisContainer = | ||
| Line 3,208: | Line 3,029: | ||
const variablesSection = | const variablesSection = | ||
this.createSection( | this.createSection('Variables'); | ||
const detectVariablesButton = | const detectVariablesButton = | ||
this.createButton( | this.createButton( | ||
| Line 3,232: | Line 3,048: | ||
); | ); | ||
variablesSection.append( | |||
$('<div>') | $('<div>') | ||
.addClass( | .addClass( | ||
| Line 3,240: | Line 3,056: | ||
detectVariablesButton, | detectVariablesButton, | ||
detectHelp | detectHelp | ||
) | ) | ||
); | ); | ||
| Line 3,321: | Line 3,134: | ||
); | ); | ||
this.workspace.append( | this.workspace.append(layout); | ||
this.reanalyze(); | this.reanalyze(); | ||
| Line 3,363: | Line 3,174: | ||
}, | }, | ||
detectVariables() { | detectVariables() { | ||
const source = | const source = | ||
| Line 3,374: | Line 3,181: | ||
source; | source; | ||
this.state.dirty = | this.state.dirty = true; | ||
this.reanalyze(); | this.reanalyze(); | ||
this.renderVariablesBySelector(); | this.renderVariablesBySelector(); | ||
this.renderVariableConfigurationBySelector(); | this.renderVariableConfigurationBySelector(); | ||
this.renderAnalysisBySelector(); | this.renderAnalysisBySelector(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
this.updateEditorTools(); | |||
this. | this.saveDraft(); | ||
this.setStatus( | |||
this.setStatus( | |||
'Variables detected from the current preload.', | 'Variables detected from the current preload.', | ||
'success' | 'success' | ||
| Line 3,432: | Line 3,204: | ||
const content = | const content = | ||
this.extractIncludeonly( | this.extractIncludeonly(source); | ||
const analysis = | const analysis = | ||
this.analyzePreload( | this.analyzePreload(content); | ||
this.state.preloadContent = | this.state.preloadContent = | ||
| Line 3,464: | Line 3,232: | ||
analysis.variables.filter( | analysis.variables.filter( | ||
name => | name => | ||
name !== | name !== 'PAGE' | ||
) | ) | ||
); | ); | ||
| Line 3,480: | Line 3,247: | ||
}, | }, | ||
renderAnalysis( | renderAnalysis(container) { | ||
container.empty(); | container.empty(); | ||
| Line 3,559: | Line 3,324: | ||
); | ); | ||
container.append( | container.append(summary); | ||
if ( | if ( | ||
analysis.conditionVariables | analysis.conditionVariables.length | ||
) { | ) { | ||
container.append( | container.append( | ||
| Line 3,574: | Line 3,336: | ||
.text( | .text( | ||
'Condition variables: ' + | 'Condition variables: ' + | ||
analysis | analysis.conditionVariables.join(', ') | ||
) | ) | ||
); | ); | ||
| Line 3,587: | Line 3,347: | ||
); | ); | ||
if ( | if (validation.errors.length) { | ||
const errors = | const errors = | ||
$('<ul>') | $('<ul>') | ||
| Line 3,600: | Line 3,358: | ||
$('<li>') | $('<li>') | ||
.text(error) | .text(error) | ||
.appendTo( | .appendTo(errors); | ||
} | } | ||
); | ); | ||
container.append( | container.append(errors); | ||
} | } | ||
if ( | if (validation.warnings.length) { | ||
const warnings = | const warnings = | ||
$('<ul>') | $('<ul>') | ||
| Line 3,624: | Line 3,376: | ||
$('<li>') | $('<li>') | ||
.text(warning) | .text(warning) | ||
.appendTo( | .appendTo(warnings); | ||
} | } | ||
); | ); | ||
container.append( | container.append(warnings); | ||
} | } | ||
| Line 3,662: | Line 3,410: | ||
container.length | container.length | ||
) { | ) { | ||
this.renderAnalysis( | this.renderAnalysis(container); | ||
} | } | ||
}, | }, | ||
/ | // Variables | ||
renderVariables( | renderVariables(container) { | ||
container.empty(); | container.empty(); | ||
| Line 3,695: | Line 3,437: | ||
if ( | if ( | ||
name !== 'PAGE' && | name !== 'PAGE' && | ||
!ordered.includes( | !ordered.includes(name) | ||
) { | ) { | ||
ordered.push( | ordered.push(name); | ||
} | } | ||
} | } | ||
); | ); | ||
const configuredFields = | const configuredFields = | ||
this.state.config && | this.state.config && | ||
| Line 3,725: | Line 3,459: | ||
if ( | if ( | ||
upper !== 'PAGE' && | upper !== 'PAGE' && | ||
!ordered.includes( | !ordered.includes(upper) | ||
) { | ) { | ||
ordered.push( | 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); | |||
} | } | ||
} | } | ||
| Line 3,746: | Line 3,497: | ||
const isSelected = | const isSelected = | ||
name === | name === | ||
this.state | this.state.selectedVariable; | ||
const isUsed = | const isUsed = | ||
analysis.variables.includes( | analysis.variables.includes( | ||
name | name | ||
); | |||
const isConfigured = | |||
!!( | |||
this.state.config && | |||
this.state.config.fields && | |||
this.state.config.fields[name] | |||
); | ); | ||
| Line 3,776: | Line 3,533: | ||
); | ); | ||
$('<span>') | |||
.addClass( | |||
'preload-editor-variable-name' | |||
) | |||
.text(name) | |||
.appendTo(item); | |||
if (name === 'PAGE') { | |||
if ( | |||
item.append( | item.append( | ||
$('<span>') | $('<span>') | ||
.addClass( | .addClass( | ||
'preload-editor-badge' | 'preload-editor-badge' | ||
) | |||
.text('AUTOMATIC') | |||
); | |||
} else if (!isConfigured) { | |||
item.append( | |||
$('<span>') | |||
.addClass( | |||
'preload-editor-badge is-warning' | |||
) | ) | ||
.text( | .text( | ||
' | isUsed | ||
? 'NOT CONFIGURED' | |||
: 'DELETED' | |||
) | ) | ||
); | ); | ||
} else if ( | } else if (!isUsed) { | ||
item.append( | item.append( | ||
$('<span>') | $('<span>') | ||
| Line 3,808: | Line 3,566: | ||
'preload-editor-badge is-warning' | 'preload-editor-badge is-warning' | ||
) | ) | ||
.text( | .text('UNUSED') | ||
); | ); | ||
} | } | ||
list.append( | list.append(item); | ||
} | } | ||
); | ); | ||
container.append( | container.append(list); | ||
if ( | if (!ordered.length) { | ||
container.append( | container.append( | ||
$('<div>') | $('<div>') | ||
| Line 3,850: | Line 3,600: | ||
container.length | container.length | ||
) { | ) { | ||
this.renderVariables( | this.renderVariables(container); | ||
} | } | ||
}, | }, | ||
selectVariable(name) { | |||
selectVariable( | |||
name = | name = | ||
String(name || '') | String(name || '') | ||
| Line 3,879: | Line 3,619: | ||
this.analysis && | this.analysis && | ||
this.analysis.variables | this.analysis.variables | ||
? this.analysis | ? this.analysis.variables | ||
: [] | : [] | ||
).filter( | ).filter( | ||
variable => | variable => | ||
variable !== | variable !== 'PAGE' | ||
) | ) | ||
); | ); | ||
if ( | 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; | return; | ||
} | } | ||
| Line 3,901: | Line 3,658: | ||
this.renderVariablesBySelector(); | this.renderVariablesBySelector(); | ||
this.renderVariableConfigurationBySelector(); | this.renderVariableConfigurationBySelector(); | ||
}, | }, | ||
| Line 3,922: | Line 3,678: | ||
}, | }, | ||
renderVariableConfiguration( | renderVariableConfiguration(container) { | ||
container.empty(); | container.empty(); | ||
| Line 3,946: | Line 3,700: | ||
const field = | const field = | ||
fields[name] | fields[name]; | ||
if ( | if (name === 'PAGE') { | ||
container.append( | container.append( | ||
this.createAutomaticPageCard( | this.createAutomaticPageCard( | ||
field | field || | ||
this.getAutomaticPageField() | |||
) | ) | ||
); | ); | ||
| Line 3,965: | Line 3,713: | ||
} | } | ||
if (!field) { | |||
const missing = | |||
$('<div>') | |||
.addClass( | |||
'preload-editor-variable preload-editor-variable-missing' | |||
); | |||
$('<div>') | |||
.addClass( | |||
'preload-editor-variable-header' | |||
) | |||
.append( | |||
field | $('<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 = | const card = | ||
$('<div>') | $('<div>') | ||
| Line 3,998: | Line 3,778: | ||
header.append( | header.append( | ||
$('<h3>') | $('<h3>').text('PAGE'), | ||
$('<span>') | $('<span>') | ||
| Line 4,005: | Line 3,784: | ||
'preload-editor-badge' | 'preload-editor-badge' | ||
) | ) | ||
.text( | .text('AUTOMATIC') | ||
); | ); | ||
card.append( | card.append(header); | ||
$('<p>') | $('<p>') | ||
| Line 4,021: | Line 3,796: | ||
'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.' | 'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.' | ||
) | ) | ||
.appendTo( | .appendTo(card); | ||
const label = | const label = | ||
| Line 4,049: | Line 3,822: | ||
const type = | const type = | ||
this.textInput( | this.textInput( | ||
field.type || | field.type || 'page' | ||
).prop( | ).prop( | ||
'disabled', | 'disabled', | ||
| Line 4,058: | Line 3,830: | ||
const required = | const required = | ||
this.checkbox( | this.checkbox( | ||
field.required !== | field.required !== false | ||
).prop( | ).prop( | ||
'disabled', | 'disabled', | ||
| Line 4,067: | Line 3,838: | ||
const namespace = | const namespace = | ||
this.textInput( | this.textInput( | ||
field.namespace || | field.namespace || 'Main' | ||
).prop( | ).prop( | ||
'disabled', | 'disabled', | ||
| Line 4,076: | Line 3,846: | ||
const allowNew = | const allowNew = | ||
this.checkbox( | this.checkbox( | ||
field['allow-new'] === | field['allow-new'] === true | ||
).prop( | ).prop( | ||
'disabled', | 'disabled', | ||
| Line 4,140: | Line 3,909: | ||
header.append( | header.append( | ||
$('<h3>') | $('<h3>').text(name) | ||
); | ); | ||
if ( | if ( | ||
!analysis.variables.includes( | !analysis.variables.includes(name) | ||
) { | ) { | ||
header.append( | header.append( | ||
| Line 4,154: | Line 3,920: | ||
'preload-editor-badge is-warning' | 'preload-editor-badge is-warning' | ||
) | ) | ||
.text( | .text('UNUSED') | ||
); | ); | ||
} | } | ||
card.append( | card.append(header); | ||
const label = | const label = | ||
| Line 4,176: | Line 3,938: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,190: | Line 3,951: | ||
const description = | const description = | ||
this.textarea( | this.textarea( | ||
field.description || | field.description || '' | ||
).addClass( | ).addClass( | ||
'preload-editor-small-textarea' | 'preload-editor-small-textarea' | ||
| Line 4,203: | Line 3,963: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,216: | Line 3,975: | ||
const types = [ | const types = [ | ||
[ | ['text', 'Text'], | ||
['textarea', 'Textarea'], | |||
['number', 'Number'], | |||
['boolean', 'Boolean'], | |||
[ | ['select', 'Select'], | ||
['multiselect', 'Multi-select'], | |||
['page', 'Page'], | |||
['user', 'User'], | |||
[ | ['date', 'Date'] | ||
[ | |||
[ | |||
[ | |||
[ | |||
[ | |||
[ | |||
]; | ]; | ||
| Line 4,257: | Line 3,989: | ||
this.select( | this.select( | ||
types.map( | types.map( | ||
( | ([value, label]) => ({ | ||
value, | value, | ||
label | label | ||
}) | }) | ||
), | ), | ||
field.type || | field.type || 'text' | ||
); | ); | ||
| Line 4,280: | Line 4,006: | ||
this.renderVariableConfigurationBySelector(); | this.renderVariableConfigurationBySelector(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,294: | Line 4,019: | ||
const required = | const required = | ||
this.checkbox( | this.checkbox( | ||
field.required === | field.required === true | ||
); | ); | ||
| Line 4,302: | Line 4,026: | ||
() => { | () => { | ||
field.required = | field.required = | ||
required.prop( | required.prop('checked'); | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,321: | Line 4,042: | ||
const placeholder = | const placeholder = | ||
this.textInput( | this.textInput( | ||
field.placeholder || | field.placeholder || '' | ||
); | ); | ||
| Line 4,339: | Line 4,059: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,353: | Line 4,072: | ||
const defaultInput = | const defaultInput = | ||
this.textInput( | this.textInput( | ||
field.default === | field.default === undefined | ||
? '' | ? '' | ||
: field.default | : field.default | ||
| Line 4,373: | Line 4,091: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,387: | Line 4,104: | ||
if ( | if ( | ||
field.type === | field.type === 'textarea' | ||
) { | ) { | ||
const maxRows = | const maxRows = | ||
this.textInput( | this.textInput( | ||
field.maxRows || | field.maxRows || '' | ||
).attr( | ).attr( | ||
'type', | 'type', | ||
| Line 4,409: | Line 4,124: | ||
if ( | if ( | ||
Number.isFinite( | Number.isFinite(value) && | ||
value > 0 | value > 0 | ||
) { | ) { | ||
| Line 4,421: | Line 4,134: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,435: | Line 4,147: | ||
if ( | if ( | ||
field.type === | field.type === 'select' || | ||
field.type === 'multiselect' | |||
field.type === | |||
) { | ) { | ||
this.renderSelectConfiguration( | this.renderSelectConfiguration( | ||
| Line 4,446: | Line 4,156: | ||
} | } | ||
return | /* | ||
* DELETE VARIABLE | |||
* | |||
* This removes the field from the generated | |||
* config.json, but does not remove %%VARIABLE%% | |||
* from the preload itself. | |||
*/ | |||
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. | !this.state.deletedVariables.includes( | ||
name | |||
) | |||
) { | |||
this.state.deletedVariables.push( | |||
name | |||
); | ); | ||
} | |||
this.state.selectedVariable = | |||
'input.preloadEditor', | name; | ||
this.markDirty(); | |||
this.renderVariablesBySelector(); | |||
this.renderVariableConfigurationBySelector(); | |||
this.renderAnalysisBySelector(); | |||
this.renderConfigPreviewBySelector(); | |||
this.setStatus( | |||
'Variable "' + | |||
name + | |||
'" removed from the configuration.', | |||
'warning' | |||
); | |||
}, | |||
renderSelectConfiguration( | |||
card, | |||
field | |||
) { | |||
const source = | |||
this.textInput( | |||
field.source || '', | |||
'category:Example' | |||
); | |||
source.on( | |||
'input.preloadEditor', | |||
() => { | () => { | ||
const value = | const value = | ||
source.val(); | |||
if (value) { | if (value) { | ||
field | field.source = value; | ||
} else { | } else { | ||
delete field | delete field.source; | ||
} | } | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,516: | Line 4,284: | ||
card.append( | card.append( | ||
this.fieldRow( | this.fieldRow( | ||
'Source | 'Source', | ||
source, | |||
' | 'Currently supported source format: category:Category Name' | ||
) | ) | ||
); | ); | ||
const | const sourcePrefix = | ||
this.textInput( | this.textInput( | ||
field | field['source-prefix'] || '' | ||
); | ); | ||
sourcePrefix.on( | |||
'input.preloadEditor', | 'input.preloadEditor', | ||
() => { | () => { | ||
const value = | const value = | ||
excludeCategory.val(); | sourcePrefix.val(); | ||
if (value) { | |||
field['source-prefix'] = | |||
value; | |||
} else { | |||
delete field['source-prefix']; | |||
} | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
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) { | if (value) { | ||
| Line 4,539: | Line 4,338: | ||
Object.assign( | Object.assign( | ||
{}, | {}, | ||
field.exclude || | field.exclude || {}, | ||
{ | { | ||
category: | category: value | ||
} | } | ||
); | ); | ||
| Line 4,549: | Line 4,346: | ||
field.exclude | field.exclude | ||
) { | ) { | ||
delete field.exclude | delete field.exclude.category; | ||
if ( | if ( | ||
| Line 4,562: | Line 4,358: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,589: | Line 4,384: | ||
const optionsValue = | const optionsValue = | ||
Array.isArray( | Array.isArray(field.options) | ||
? field.options | ? field.options | ||
.map( | .map( | ||
| Line 4,604: | Line 4,397: | ||
return ( | return ( | ||
String( | String( | ||
option.value || | option.value || '' | ||
) + | ) + | ||
' | ' + | ' | ' + | ||
| Line 4,637: | Line 4,429: | ||
line.trim() | line.trim() | ||
) | ) | ||
.filter( | .filter(Boolean); | ||
if (!lines.length) { | if (!lines.length) { | ||
| Line 4,665: | Line 4,455: | ||
label: | label: | ||
parts | parts | ||
.slice( | .slice(1) | ||
.join( | .join( | ||
' | ' | ' | ' | ||
| Line 4,677: | Line 4,465: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
} | } | ||
| Line 4,704: | Line 4,491: | ||
$('<h4>') | $('<h4>') | ||
.text(title) | .text(title) | ||
.appendTo( | .appendTo(wrapper); | ||
const existing = | const existing = | ||
field[property] && | field[property] && | ||
typeof field[property] === | typeof field[property] === 'object' | ||
? field[property] | ? field[property] | ||
: {}; | : {}; | ||
| Line 4,741: | Line 4,525: | ||
} | } | ||
], | ], | ||
existing.transform || | existing.transform || '' | ||
); | ); | ||
const prefix = | const prefix = | ||
this.textInput( | this.textInput( | ||
existing.prefix || | existing.prefix || '' | ||
); | ); | ||
const suffix = | const suffix = | ||
this.textInput( | this.textInput( | ||
existing.suffix || | existing.suffix || '' | ||
); | ); | ||
const from = | const from = | ||
this.textInput( | this.textInput( | ||
existing.from || | existing.from || '' | ||
); | ); | ||
const to = | const to = | ||
this.textInput( | this.textInput( | ||
existing.to || | existing.to || '' | ||
); | ); | ||
| Line 4,805: | Line 4,584: | ||
prefixRow.toggle( | prefixRow.toggle( | ||
type === | type === 'remove-prefix' | ||
); | ); | ||
suffixRow.toggle( | suffixRow.toggle( | ||
type === | type === 'remove-suffix' | ||
); | ); | ||
fromRow.toggle( | fromRow.toggle( | ||
type === | type === 'replace' | ||
); | ); | ||
toRow.toggle( | toRow.toggle( | ||
type === | type === 'replace' | ||
); | ); | ||
}; | }; | ||
| Line 4,830: | Line 4,605: | ||
transform.val(); | transform.val(); | ||
let object = | let object = null; | ||
if ( | if ( | ||
type === | type === 'remove-prefix' && | ||
prefix.val() | prefix.val() | ||
) { | ) { | ||
object = { | object = { | ||
transform: | transform: type, | ||
prefix: | prefix: | ||
prefix.val() | prefix.val() | ||
}; | }; | ||
} else if ( | } else if ( | ||
type === | type === 'remove-suffix' && | ||
suffix.val() | suffix.val() | ||
) { | ) { | ||
object = { | object = { | ||
transform: | transform: type, | ||
suffix: | suffix: | ||
suffix.val() | suffix.val() | ||
}; | }; | ||
} else if ( | } else if ( | ||
type === | type === 'replace' && | ||
from.val() | from.val() | ||
) { | ) { | ||
object = { | object = { | ||
transform: | transform: type, | ||
from: | from: | ||
from.val(), | from.val(), | ||
| Line 4,871: | Line 4,639: | ||
if (object) { | if (object) { | ||
field[property] = | field[property] = object; | ||
} else { | } else { | ||
delete field[property]; | delete field[property]; | ||
| Line 4,878: | Line 4,645: | ||
this.markDirty(); | this.markDirty(); | ||
this.renderConfigPreviewBySelector(); | this.renderConfigPreviewBySelector(); | ||
}; | }; | ||
| Line 4,918: | Line 4,684: | ||
renderInputs(); | renderInputs(); | ||
wrapper.append( | wrapper.append(controls); | ||
return wrapper; | return wrapper; | ||
| Line 4,938: | Line 4,702: | ||
config.fields || {}; | config.fields || {}; | ||
const orderedFields = | const orderedFields = {}; | ||
orderedFields.PAGE = | orderedFields.PAGE = | ||
this.getAutomaticPageField(); | this.getAutomaticPageField(); | ||
Object.keys( | Object.keys(fields) | ||
.filter( | .filter( | ||
name => | name => | ||
name.toUpperCase() !== | name.toUpperCase() !== 'PAGE' | ||
) | ) | ||
.forEach( | .forEach( | ||
| Line 4,976: | Line 4,733: | ||
}, | }, | ||
renderConfigPreview( | renderConfigPreview(container) { | ||
if (container) { | if (container) { | ||
container.text( | container.text( | ||
| Line 5,006: | Line 4,761: | ||
markDirty() { | markDirty() { | ||
this.state.dirty = | this.state.dirty = true; | ||
this.saveDraft(); | this.saveDraft(); | ||
}, | }, | ||
| Line 5,021: | Line 4,770: | ||
let title = | let title = | ||
String( | String( | ||
this.state.preloadTitle || | this.state.preloadTitle || '' | ||
).trim(); | ).trim(); | ||
| Line 5,037: | Line 4,785: | ||
title = result; | title = result; | ||
this.state.preloadTitle = | this.state.preloadTitle = | ||
title; | title; | ||
} | } | ||
this.state.preloadContent = | this.state.preloadContent = | ||
this.getEditorContent(); | this.getEditorContent(); | ||
| Line 5,052: | Line 4,795: | ||
this.validateBeforeSave(); | this.validateBeforeSave(); | ||
if ( | if (validation.errors.length) { | ||
this.showValidationErrors( | this.showValidationErrors( | ||
validation | validation | ||
); | ); | ||
this.saveDraft(); | this.saveDraft(); | ||
return; | return; | ||
} | } | ||
| Line 5,075: | Line 4,811: | ||
try { | try { | ||
const exists = | const exists = | ||
await this.pageExists( | await this.pageExists(title); | ||
await this.savePage( | await this.savePage( | ||
| Line 5,088: | Line 4,822: | ||
); | ); | ||
this.state.mode = | this.state.mode = 'edit'; | ||
this.state.dirty = false; | |||
this.clearDraft(title); | |||
this.clearDraft( | |||
this.setStatus( | this.setStatus( | ||
| Line 5,111: | Line 4,835: | ||
'Preload saved.', | 'Preload saved.', | ||
{ | { | ||
type: | type: 'success' | ||
} | } | ||
); | ); | ||
} catch ( | } catch (error) { | ||
console.error( | console.error( | ||
'Preload Editor save:', | 'Preload Editor save:', | ||
| Line 5,123: | Line 4,844: | ||
); | ); | ||
this.saveDraft(); | this.saveDraft(); | ||
| Line 5,139: | Line 4,856: | ||
'Could not save preload.', | 'Could not save preload.', | ||
{ | { | ||
type: | type: 'error' | ||
} | } | ||
); | ); | ||
| Line 5,149: | Line 4,865: | ||
let title = | let title = | ||
String( | String( | ||
this.state.preloadTitle || | this.state.preloadTitle || '' | ||
).trim(); | ).trim(); | ||
| Line 5,165: | Line 4,880: | ||
title = result; | title = result; | ||
this.state.preloadTitle = | this.state.preloadTitle = | ||
title; | title; | ||
} | } | ||
this.state.preloadContent = | this.state.preloadContent = | ||
this.getEditorContent(); | this.getEditorContent(); | ||
this.reanalyze(); | this.reanalyze(); | ||
| Line 5,187: | Line 4,892: | ||
this.validateBeforeSave(); | this.validateBeforeSave(); | ||
if ( | if (validation.errors.length) { | ||
this.showValidationErrors( | this.showValidationErrors( | ||
validation | validation | ||
); | ); | ||
this.saveDraft(); | this.saveDraft(); | ||
return; | return; | ||
} | } | ||
| Line 5,216: | Line 4,915: | ||
try { | try { | ||
const preloadExists = | const preloadExists = | ||
await this.pageExists( | await this.pageExists(title); | ||
await this.savePage( | await this.savePage( | ||
| Line 5,235: | Line 4,932: | ||
await this.savePage( | await this.savePage( | ||
configTitle, | configTitle, | ||
this.getConfigJSON(), | this.getConfigJSON(), | ||
configExists | configExists | ||
? 'Updated preload config with Preload Editor' | ? 'Updated preload config with Preload Editor' | ||
: 'Created preload config with Preload Editor', | : 'Created preload config with Preload Editor', | ||
!configExists | !configExists | ||
); | ); | ||
this.state.mode = | this.state.mode = 'edit'; | ||
this.state.dirty = false; | |||
this.state.preloadTitle = title; | |||
this.state.dirty = | |||
this.clearDraft(title); | |||
this.state.preloadTitle = | this.setStatus( | ||
'Preload and config saved successfully.', | |||
'success' | |||
); | |||
mw.notify( | |||
'Preload and config saved.', | |||
this.clearDraft( | { | ||
type: 'success' | |||
} | |||
); | |||
this.setStatus( | } catch (error) { | ||
'Preload and config saved successfully.', | |||
'success' | |||
); | |||
mw.notify( | |||
'Preload and config saved.', | |||
{ | |||
type: | |||
} | |||
); | |||
} catch ( | |||
console.error( | console.error( | ||
'Preload Editor save:', | 'Preload Editor save:', | ||
| Line 5,280: | Line 4,963: | ||
); | ); | ||
this.saveDraft(); | this.saveDraft(); | ||
| Line 5,297: | Line 4,975: | ||
'Could not save preload/config.', | 'Could not save preload/config.', | ||
{ | { | ||
type: | type: 'error' | ||
} | } | ||
); | ); | ||
| Line 5,310: | Line 4,987: | ||
const title = | const title = | ||
String( | String( | ||
this.state.preloadTitle || | this.state.preloadTitle || '' | ||
).trim(); | ).trim(); | ||
| Line 5,322: | Line 4,998: | ||
if ( | if ( | ||
title && | title && | ||
!/\/Preload$/i.test( | !/\/Preload$/i.test(title) | ||
) { | ) { | ||
warnings.push( | warnings.push( | ||
| Line 5,344: | Line 5,018: | ||
if ( | if ( | ||
!/<includeonly>/i.test( | !/<includeonly>/i.test(content) | ||
) { | ) { | ||
warnings.push( | warnings.push( | ||
| Line 5,354: | Line 5,026: | ||
if ( | if ( | ||
!/<noinclude>/i.test( | !/<noinclude>/i.test(content) | ||
) { | ) { | ||
warnings.push( | warnings.push( | ||
| Line 5,370: | Line 5,040: | ||
); | ); | ||
this.state.config = | this.state.config = | ||
this.prepareConfigForVariables( | this.prepareConfigForVariables( | ||
| Line 5,400: | Line 5,066: | ||
condition => { | condition => { | ||
if ( | if ( | ||
condition.type === | condition.type === 'if' | ||
) { | ) { | ||
depth++; | depth++; | ||
} else if ( | } else if ( | ||
condition.type === | condition.type === 'endif' | ||
) { | ) { | ||
depth--; | depth--; | ||
if ( | if (depth < 0) { | ||
errors.push( | errors.push( | ||
'Found @endif without a matching @if.' | 'Found @endif without a matching @if.' | ||
| Line 5,420: | Line 5,082: | ||
} | } | ||
} else if ( | } else if ( | ||
condition.type === | condition.type === 'elseif' || | ||
condition.type === 'else' | |||
condition.type === | |||
) { | ) { | ||
if ( | if (depth === 0) { | ||
errors.push( | errors.push( | ||
'@' + | '@' + | ||
| Line 5,452: | Line 5,110: | ||
}, | }, | ||
showValidationErrors( | showValidationErrors(validation) { | ||
this.renderAnalysisBySelector(); | this.renderAnalysisBySelector(); | ||
this.saveDraft(); | this.saveDraft(); | ||
this.setStatus( | this.setStatus( | ||
validation.errors.join( | validation.errors.join(' '), | ||
'error' | 'error' | ||
); | ); | ||
mw.notify( | mw.notify( | ||
validation.errors.join( | validation.errors.join('\n'), | ||
{ | { | ||
type: | type: 'error' | ||
} | } | ||
); | ); | ||
| Line 5,487: | Line 5,134: | ||
$(function () { | $(function () { | ||
if ( | if ( | ||
mw.config.get( | mw.config.get('wgPageName') !== | ||
'Petit_Planet:Preload_Editor' | 'Petit_Planet:Preload_Editor' | ||
) { | ) { | ||