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 31: | Line 31: | ||
// API | // API | ||
async getPage(title) { | |||
const response = await new mw.Api().get({ | |||
action: 'query', | |||
prop: '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 : '' | |||
}; | |||
}, | |||
async pageExists(title) { | async pageExists(title) { | ||
| Line 76: | Line 76: | ||
}); | }); | ||
return response | return ( | ||
response && | |||
response.query && | |||
response.query.search | |||
) || []; | |||
}, | }, | ||
| Line 99: | Line 103: | ||
/<includeonly>([\s\S]*?)<\/includeonly>/i | /<includeonly>([\s\S]*?)<\/includeonly>/i | ||
); | ); | ||
return match ? match[1].trim() : String(source || '').trim(); | |||
return match | |||
? match[1].trim() | |||
: String(source || '').trim(); | |||
}, | }, | ||
| Line 109: | Line 116: | ||
while ((match = pattern.exec(content)) !== null) { | while ((match = pattern.exec(content)) !== null) { | ||
const name = match[1].toUpperCase(); | const name = match[1].toUpperCase(); | ||
if (!variables.includes(name)) variables.push(name); | |||
if (!variables.includes(name)) { | |||
variables.push(name); | |||
} | |||
} | } | ||
| Line 119: | Line 129: | ||
const pattern = | const pattern = | ||
/<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi; | /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi; | ||
let match; | let match; | ||
| Line 136: | Line 147: | ||
this.findConditions(content).forEach(condition => { | this.findConditions(content).forEach(condition => { | ||
if (condition.type !== 'if' && | if ( | ||
condition.type !== 'elseif') return; | condition.type !== 'if' && | ||
condition.type !== 'elseif' | |||
) { | |||
return; | |||
} | |||
const matches = | const matches = | ||
condition.expression.match(/[A-Za-z0-9_-]+/g) || []; | condition.expression.match( | ||
/[A-Za-z0-9_-]+/g | |||
) || []; | |||
matches.forEach(name => { | matches.forEach(name => { | ||
name = name.toUpperCase(); | name = name.toUpperCase(); | ||
if (!variables.includes(name)) variables.push(name); | |||
if (!variables.includes(name)) { | |||
variables.push(name); | |||
} | |||
}); | }); | ||
}); | }); | ||
| Line 155: | Line 175: | ||
variables: this.findVariables(content), | variables: this.findVariables(content), | ||
conditions: this.findConditions(content), | conditions: this.findConditions(content), | ||
conditionVariables: this.findConditionVariables(content) | conditionVariables: | ||
this.findConditionVariables(content) | |||
}; | }; | ||
}, | }, | ||
| Line 163: | Line 184: | ||
emptyConfig() { | emptyConfig() { | ||
return { | return { | ||
settings: { incomplete: 'add-stub' }, | settings: { | ||
incomplete: 'add-stub' | |||
}, | |||
fields: { | fields: { | ||
PAGE: { | PAGE: { | ||
| Line 178: | Line 201: | ||
normalizeConfig(config) { | normalizeConfig(config) { | ||
if (!config || | if ( | ||
!config || | |||
typeof config !== 'object' || | typeof config !== 'object' || | ||
Array.isArray(config)) { | Array.isArray(config) | ||
) { | |||
config = this.emptyConfig(); | config = this.emptyConfig(); | ||
} | } | ||
if (!config.settings || | if ( | ||
!config.settings || | |||
typeof config.settings !== 'object' || | typeof config.settings !== 'object' || | ||
Array.isArray(config.settings)) { | Array.isArray(config.settings) | ||
) { | |||
config.settings = {}; | config.settings = {}; | ||
} | } | ||
if (!config.fields || | if ( | ||
!config.fields || | |||
typeof config.fields !== 'object' || | typeof config.fields !== 'object' || | ||
Array.isArray(config.fields)) { | Array.isArray(config.fields) | ||
) { | |||
config.fields = {}; | config.fields = {}; | ||
} | } | ||
| Line 214: | Line 243: | ||
label: this.humanizeName(name), | label: this.humanizeName(name), | ||
description: '', | description: '', | ||
type: analysis.conditionVariables.includes(name) | type: | ||
analysis.conditionVariables.includes(name) | |||
? 'boolean' | |||
: 'text' | |||
}; | }; | ||
}, | }, | ||
| Line 226: | Line 256: | ||
.replace(/\s+/g, ' ') | .replace(/\s+/g, ' ') | ||
.trim() | .trim() | ||
.replace(/\b\w/g, char => char.toUpperCase()); | .replace( | ||
/\b\w/g, | |||
char => char.toUpperCase() | |||
); | |||
}, | }, | ||
| Line 246: | Line 279: | ||
analysis.variables.forEach(name => { | analysis.variables.forEach(name => { | ||
if (name === 'PAGE') return; | if (name === 'PAGE') return; | ||
fields[name] = | fields[name] = | ||
existing[name] || | existing[name] || | ||
this.createDefaultField(name, analysis); | this.createDefaultField( | ||
name, | |||
analysis | |||
); | |||
}); | }); | ||
Object.keys(existing).forEach(name => { | Object.keys(existing).forEach(name => { | ||
const upper = name.toUpperCase(); | const upper = name.toUpperCase(); | ||
if (!fields[upper]) fields[upper] = existing[name]; | |||
if (!fields[upper]) { | |||
fields[upper] = existing[name]; | |||
} | |||
}); | }); | ||
config.fields = fields; | config.fields = fields; | ||
return config; | return config; | ||
}, | }, | ||
| Line 264: | Line 305: | ||
const warnings = []; | const warnings = []; | ||
if (!config || | if ( | ||
!config || | |||
typeof config !== 'object' || | typeof config !== 'object' || | ||
Array.isArray(config)) { | Array.isArray(config) | ||
) { | |||
return { | return { | ||
valid: false, | valid: false, | ||
| Line 274: | Line 317: | ||
} | } | ||
if (!config.fields || | if ( | ||
!config.fields || | |||
typeof config.fields !== 'object' || | typeof config.fields !== 'object' || | ||
Array.isArray(config.fields)) { | Array.isArray(config.fields) | ||
) { | |||
return { | return { | ||
valid: false, | valid: false, | ||
| Line 285: | Line 330: | ||
const configured = | const configured = | ||
Object.keys(config.fields).map(name => name.toUpperCase()); | Object.keys(config.fields).map( | ||
name => name.toUpperCase() | |||
); | |||
analysis.variables.forEach(name => { | analysis.variables.forEach(name => { | ||
if (!configured.includes(name)) { | if (!configured.includes(name)) { | ||
errors.push( | errors.push( | ||
'Variable "' + name + '" is not configured.' | 'Variable "' + | ||
name + | |||
'" is not configured.' | |||
); | ); | ||
} | } | ||
| Line 298: | Line 347: | ||
if (!analysis.variables.includes(name)) { | if (!analysis.variables.includes(name)) { | ||
warnings.push( | warnings.push( | ||
'Field "' + name + '" is not used by the preload.' | 'Field "' + | ||
name + | |||
'" is not used by the preload.' | |||
); | ); | ||
} | } | ||
| Line 304: | Line 355: | ||
if (!configured.includes('PAGE')) { | if (!configured.includes('PAGE')) { | ||
errors.push('PAGE must always be configured.'); | errors.push( | ||
'PAGE must always be configured.' | |||
); | |||
} | } | ||
Object.entries(config.fields).forEach(([name, field]) => { | 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 { | return { | ||
| Line 344: | Line 411: | ||
// Transformation helpers | // Transformation helpers | ||
transformationObject(transform, prefix, suffix, from, to) { | transformationObject( | ||
transform, | |||
prefix, | |||
suffix, | |||
from, | |||
to | |||
) { | |||
if (!transform) return null; | if (!transform) return null; | ||
| Line 361: | Line 434: | ||
return from === '' | return from === '' | ||
? null | ? null | ||
: { transform, from, to: to || '' }; | : { | ||
transform, | |||
from, | |||
to: to || '' | |||
}; | |||
default: | default: | ||
| Line 374: | Line 451: | ||
.addClass('preload-editor-section'); | .addClass('preload-editor-section'); | ||
if (className) section.addClass(className); | if (className) { | ||
section.addClass(className); | |||
} | |||
$('<h2>') | $('<h2>') | ||
.addClass('preload-editor-section-title') | .addClass( | ||
'preload-editor-section-title' | |||
) | |||
.text(title) | .text(title) | ||
.appendTo(section); | .appendTo(section); | ||
| Line 390: | Line 471: | ||
.text(label); | .text(label); | ||
if (className) button.addClass(className); | if (className) { | ||
button.addClass(className); | |||
} | |||
if (callback) { | if (callback) { | ||
button.on('click.preloadEditor', callback); | button.on( | ||
'click.preloadEditor', | |||
callback | |||
); | |||
} | } | ||
| Line 415: | Line 502: | ||
row.append(input); | row.append(input); | ||
return row; | return row; | ||
}, | }, | ||
| Line 422: | Line 510: | ||
.attr({ | .attr({ | ||
type: 'text', | type: 'text', | ||
value: value == null ? '' : value, | value: | ||
placeholder: placeholder || '' | value == null | ||
? '' | |||
: value, | |||
placeholder: | |||
placeholder || '' | |||
}) | }) | ||
.addClass('preload-editor-input'); | .addClass('preload-editor-input'); | ||
| Line 430: | Line 522: | ||
textarea(value) { | textarea(value) { | ||
return $('<textarea>') | return $('<textarea>') | ||
.val(value == null ? '' : value) | .val( | ||
.addClass('preload-editor-textarea'); | value == null | ||
? '' | |||
: value | |||
) | |||
.addClass( | |||
'preload-editor-textarea' | |||
); | |||
}, | }, | ||
| Line 438: | Line 536: | ||
.attr('type', 'checkbox') | .attr('type', 'checkbox') | ||
.prop('checked', !!checked) | .prop('checked', !!checked) | ||
.addClass('preload-editor-checkbox'); | .addClass( | ||
'preload-editor-checkbox' | |||
); | |||
}, | }, | ||
select(options, value) { | select(options, value) { | ||
const select = $('<select>') | const select = $('<select>') | ||
.addClass('preload-editor-select'); | .addClass( | ||
'preload-editor-select' | |||
); | |||
options.forEach(option => { | options.forEach(option => { | ||
$('<option>') | $('<option>') | ||
.attr('value', option.value) | .attr( | ||
'value', | |||
option.value | |||
) | |||
.text(option.label) | .text(option.label) | ||
.prop( | .prop( | ||
'selected', | 'selected', | ||
String(option.value) === String(value === undefined ? '' : value) | String(option.value) === | ||
String( | |||
value === undefined | |||
? '' | |||
: value | |||
) | |||
) | ) | ||
.appendTo(select); | .appendTo(select); | ||
| Line 468: | Line 578: | ||
} | } | ||
const { view, inputHandler } = this.editor; | const { | ||
view, | |||
inputHandler | |||
} = this.editor; | |||
if (view && inputHandler) { | if (view && inputHandler) { | ||
view.dom.removeEventListener('input', inputHandler); | view.dom.removeEventListener( | ||
'input', | |||
inputHandler | |||
); | |||
} | } | ||
if (view) view.destroy(); | if (view) { | ||
view.destroy(); | |||
} | |||
this.editor = null; | this.editor = null; | ||
| Line 486: | Line 604: | ||
getEditorContent() { | getEditorContent() { | ||
if (this.editorView && this.editorView.state) { | if ( | ||
this.editorView && | |||
this.editorView.state | |||
) { | |||
return this.editorView.state.doc.toString(); | return this.editorView.state.doc.toString(); | ||
} | } | ||
| Line 501: | Line 622: | ||
} | } | ||
const current = this.editorView.state.doc.toString(); | const current = | ||
this.editorView.state.doc.toString(); | |||
if (current === content) return; | if (current === content) return; | ||
| Line 514: | Line 637: | ||
getEditorSelection() { | getEditorSelection() { | ||
if (!this.editorView | if ( | ||
!this.editorView || | |||
const selection = this.editorView.state.selection.main; | !this.editorView.state | ||
) { | |||
return null; | |||
} | |||
const selection = | |||
this.editorView.state.selection.main; | |||
return { | return { | ||
from: selection.from, | from: selection.from, | ||
to: selection.to, | to: selection.to, | ||
text: this.editorView.state.doc.sliceString( | text: | ||
this.editorView.state.doc.sliceString( | |||
selection.from, | |||
selection.to | |||
) | |||
}; | }; | ||
}, | }, | ||
focusEditor() { | focusEditor() { | ||
if (this.editorView) this.editorView.focus(); | if (this.editorView) { | ||
this.editorView.focus(); | |||
} | |||
}, | }, | ||
| Line 541: | Line 673: | ||
const view = this.editorView; | const view = this.editorView; | ||
const selection = view.state.selection.main; | const selection = | ||
view.state.selection.main; | |||
const from = | const from = | ||
| Line 559: | Line 692: | ||
view.dispatch({ | view.dispatch({ | ||
changes: { from, to, insert: text }, | changes: { | ||
selection: { anchor: cursor } | from, | ||
to, | |||
insert: text | |||
}, | |||
selection: { | |||
anchor: cursor | |||
} | |||
}); | }); | ||
| Line 569: | Line 708: | ||
wrapSelectionWithVariable() { | wrapSelectionWithVariable() { | ||
const selected = this.getEditorSelection(); | const selected = | ||
this.getEditorSelection(); | |||
if (!selected || selected.from === selected.to) { | if ( | ||
!selected || | |||
selected.from === selected.to | |||
) { | |||
this.focusEditor(); | this.focusEditor(); | ||
return; | return; | ||
| Line 577: | Line 720: | ||
const replacement = | const replacement = | ||
'%%' + selected.text + '%%'; | '%%' + | ||
selected.text + | |||
'%%'; | |||
this.insertEditorText( | this.insertEditorText( | ||
| Line 583: | Line 728: | ||
selected.from, | selected.from, | ||
selected.to, | selected.to, | ||
selected.from + replacement.length | selected.from + | ||
replacement.length | |||
); | ); | ||
}, | }, | ||
insertVariable(name) { | insertVariable(name) { | ||
name = String(name || '').trim().toUpperCase(); | name = String(name || '') | ||
.trim() | |||
.toUpperCase(); | |||
if (!name) return; | if (!name) return; | ||
const selected = this.getEditorSelection(); | const selected = | ||
this.getEditorSelection(); | |||
if (!selected) return; | if (!selected) return; | ||
const text = '%%' + name + '%%'; | const text = | ||
'%%' + | |||
name + | |||
'%%'; | |||
this.insertEditorText( | this.insertEditorText( | ||
| Line 604: | Line 758: | ||
insertCondition(type, variable) { | insertCondition(type, variable) { | ||
type = String(type || '').toLowerCase(); | type = | ||
variable = String(variable || '').trim().toUpperCase(); | String(type || '') | ||
.toLowerCase(); | |||
variable = | |||
String(variable || '') | |||
.trim() | |||
.toUpperCase(); | |||
const selected = | |||
this.getEditorSelection(); | |||
if (!selected) return; | if (!selected) return; | ||
| Line 615: | Line 777: | ||
switch (type) { | switch (type) { | ||
case 'if': { | case 'if': { | ||
if (!variable) return; | |||
if (selected.from !== selected.to) { | const opening = | ||
'<!-- @if ' + | |||
variable + | |||
' -->'; | |||
const closing = | |||
'<!-- @endif -->'; | |||
if ( | |||
selected.from !== | |||
selected.to | |||
) { | |||
text = | text = | ||
opening + | opening + | ||
| Line 625: | Line 797: | ||
'\n' + | '\n' + | ||
closing; | closing; | ||
cursorOffset = text.length; | |||
cursorOffset = | |||
text.length; | |||
} else { | } else { | ||
text = | text = | ||
| Line 631: | Line 805: | ||
'\n\n' + | '\n\n' + | ||
closing; | closing; | ||
cursorOffset = opening.length + 1; | |||
cursorOffset = | |||
opening.length + 1; | |||
} | } | ||
| Line 660: | Line 836: | ||
selected.from, | selected.from, | ||
selected.to, | selected.to, | ||
selected.from + cursorOffset | selected.from + | ||
cursorOffset | |||
); | ); | ||
}, | }, | ||
getToolVariables() { | getToolVariables() { | ||
const variables = new Set(['PAGE']); | const variables = | ||
new Set(['PAGE']); | |||
if (this.analysis && this.analysis.variables) { | if ( | ||
this.analysis.variables.forEach(name => { | this.analysis && | ||
this.analysis.variables | |||
) { | |||
this.analysis.variables.forEach( | |||
name => { | |||
variables.add( | |||
String(name).toUpperCase() | |||
); | |||
} | |||
); | |||
} | } | ||
| Line 678: | Line 863: | ||
createToolSelect(options, className) { | createToolSelect(options, className) { | ||
const select = $('<select>') | const select = $('<select>') | ||
.addClass('preload-editor-tool-select'); | .addClass( | ||
'preload-editor-tool-select' | |||
); | |||
if (className) select.addClass(className); | if (className) { | ||
select.addClass(className); | |||
} | |||
options.forEach(option => { | options.forEach(option => { | ||
$('<option>') | $('<option>') | ||
.attr('value', option.value) | .attr( | ||
'value', | |||
option.value | |||
) | |||
.text(option.label) | .text(option.label) | ||
.appendTo(select); | .appendTo(select); | ||
| Line 695: | Line 887: | ||
if (!this.editorTools) return; | if (!this.editorTools) return; | ||
const variables = this.getToolVariables(); | const variables = | ||
this.getToolVariables(); | |||
const current = | const current = | ||
this.editorTools.variableSelect.val(); | this.editorTools.variableSelect.val(); | ||
const currentCondition = | const currentCondition = | ||
this.editorTools.conditionVariableSelect.val(); | this.editorTools | ||
.conditionVariableSelect | |||
.val(); | |||
const populate = (select, value) => { | const populate = ( | ||
select, | |||
value | |||
) => { | |||
select.empty(); | select.empty(); | ||
variables.forEach(name => { | variables.forEach(name => { | ||
$('<option>') | $('<option>') | ||
.attr('value', name) | .attr( | ||
'value', | |||
name | |||
) | |||
.text(name) | .text(name) | ||
.appendTo(select); | .appendTo(select); | ||
}); | }); | ||
if (value && variables.includes(value)) { | if ( | ||
value && | |||
variables.includes(value) | |||
) { | |||
select.val(value); | select.val(value); | ||
} else if (variables.length) { | } else if (variables.length) { | ||
select.val(variables[0]); | select.val( | ||
variables[0] | |||
); | |||
} | } | ||
}; | }; | ||
| Line 724: | Line 932: | ||
populate( | populate( | ||
this.editorTools.conditionVariableSelect, | this.editorTools | ||
.conditionVariableSelect, | |||
currentCondition | currentCondition | ||
); | ); | ||
| Line 733: | Line 942: | ||
const type = | const type = | ||
this.editorTools.conditionTypeSelect.val(); | this.editorTools | ||
.conditionTypeSelect | |||
.val(); | |||
this.editorTools.conditionVariableWrap.toggle( | this.editorTools | ||
.conditionVariableWrap | |||
.toggle(type === 'if'); | |||
}, | }, | ||
renderEditorTools() { | renderEditorTools() { | ||
const tools = $('<div>') | const tools = $('<div>') | ||
.addClass('preload-editor-tools'); | .addClass( | ||
'preload-editor-tools' | |||
); | |||
// Wrap selection | // Wrap selection | ||
| Line 748: | Line 961: | ||
const wrapGroup = $('<div>') | const wrapGroup = $('<div>') | ||
.addClass( | .addClass( | ||
'preload-editor-tool-group preload-editor-tool-group-wrap' | 'preload-editor-tool-group ' + | ||
'preload-editor-tool-group-wrap' | |||
); | ); | ||
| Line 755: | Line 969: | ||
'Wrap selection with %%', | 'Wrap selection with %%', | ||
'preload-editor-tool-button', | 'preload-editor-tool-button', | ||
() => this.wrapSelectionWithVariable() | () => | ||
this.wrapSelectionWithVariable() | |||
) | ) | ||
); | ); | ||
| Line 763: | Line 978: | ||
const variableGroup = $('<div>') | const variableGroup = $('<div>') | ||
.addClass( | .addClass( | ||
'preload-editor-tool-group preload-editor-tool-group-variable' | 'preload-editor-tool-group ' + | ||
'preload-editor-tool-group-variable' | |||
); | ); | ||
$('<span>') | $('<span>') | ||
.addClass('preload-editor-tool-label') | .addClass( | ||
.text('Insert existing variable') | 'preload-editor-tool-label' | ||
) | |||
.text( | |||
'Insert existing variable' | |||
) | |||
.appendTo(variableGroup); | .appendTo(variableGroup); | ||
const variableSelect = this.createToolSelect( | const variableSelect = | ||
this.createToolSelect( | |||
[], | |||
'preload-editor-variable-select' | |||
); | |||
variableGroup.append( | variableGroup.append( | ||
| Line 781: | Line 1,002: | ||
'Insert', | 'Insert', | ||
'preload-editor-tool-button', | 'preload-editor-tool-button', | ||
() => this.insertVariable(variableSelect.val()) | () => | ||
this.insertVariable( | |||
variableSelect.val() | |||
) | |||
) | ) | ||
); | ); | ||
| Line 789: | Line 1,013: | ||
const conditionGroup = $('<div>') | const conditionGroup = $('<div>') | ||
.addClass( | .addClass( | ||
'preload-editor-tool-group preload-editor-tool-group-condition' | 'preload-editor-tool-group ' + | ||
'preload-editor-tool-group-condition' | |||
); | ); | ||
$('<span>') | $('<span>') | ||
.addClass('preload-editor-tool-label') | .addClass( | ||
'preload-editor-tool-label' | |||
) | |||
.text('Insert condition') | .text('Insert condition') | ||
.appendTo(conditionGroup); | .appendTo(conditionGroup); | ||
const conditionTypeSelect = this.createToolSelect( | 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( | const conditionVariableSelect = | ||
this.createToolSelect( | |||
[], | |||
'preload-editor-condition-variable-select' | |||
); | |||
const conditionVariableWrap = $('<span>') | const conditionVariableWrap = | ||
$('<span>') | |||
.addClass( | |||
'preload-editor-condition-variable' | |||
) | |||
.append( | |||
const insertCondition = this.createButton( | conditionVariableSelect | ||
); | |||
const insertCondition = | |||
this.createButton( | |||
'Insert', | |||
'preload-editor-tool-button', | |||
() => | |||
this.insertCondition( | |||
conditionTypeSelect.val(), | |||
conditionVariableSelect.val() | |||
) | |||
); | |||
conditionTypeSelect.on( | conditionTypeSelect.on( | ||
'change.preloadEditor', | 'change.preloadEditor', | ||
() => this.updateConditionVariableVisibility() | () => | ||
this.updateConditionVariableVisibility() | |||
); | ); | ||
| Line 867: | Line 1,114: | ||
getKnownVariables() { | getKnownVariables() { | ||
const known = new Set(['PAGE']); | const known = | ||
const fields = this.state.config | new Set(['PAGE']); | ||
const fields = | |||
this.state.config && | |||
this.state.config.fields; | |||
if (fields && typeof fields === 'object') { | if ( | ||
Object.keys(fields).forEach(name => { | fields && | ||
typeof fields === 'object' | |||
) { | |||
Object.keys(fields).forEach( | |||
name => { | |||
known.add( | |||
String(name).toUpperCase() | |||
); | |||
} | |||
); | |||
} | } | ||
| Line 879: | Line 1,137: | ||
}, | }, | ||
createSyntaxDecorations(state, Decoration) { | createSyntaxDecorations( | ||
state, | |||
Decoration | |||
) { | |||
const decorations = []; | const decorations = []; | ||
const source = state.doc.toString(); | const source = | ||
const knownVariables = this.getKnownVariables(); | state.doc.toString(); | ||
const knownVariables = | |||
this.getKnownVariables(); | |||
const variablePattern = | |||
/%%([A-Za-z0-9_-]+)%%/g; | |||
let match; | let match; | ||
while ((match = variablePattern.exec(source)) !== null) { | while ( | ||
const name = match[1].toUpperCase(); | (match = | ||
const known = knownVariables.has(name); | variablePattern.exec(source)) !== | ||
null | |||
) { | |||
const name = | |||
match[1].toUpperCase(); | |||
const known = | |||
knownVariables.has(name); | |||
decorations.push( | decorations.push( | ||
| Line 901: | Line 1,174: | ||
}).range( | }).range( | ||
match.index, | match.index, | ||
match.index + match[0].length | match.index + | ||
match[0].length | |||
) | ) | ||
); | ); | ||
| Line 909: | Line 1,183: | ||
/<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi; | /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi; | ||
while ((match = conditionPattern.exec(source)) !== null) { | while ( | ||
(match = | |||
conditionPattern.exec(source)) !== | |||
null | |||
) { | |||
decorations.push( | decorations.push( | ||
Decoration.mark({ | Decoration.mark({ | ||
class: 'preload-editor-syntax-condition' | class: | ||
'preload-editor-syntax-condition' | |||
}).range( | }).range( | ||
match.index, | match.index, | ||
match.index + match[0].length | match.index + | ||
match[0].length | |||
) | ) | ||
); | ); | ||
const directiveStart = | const directiveStart = | ||
match.index + match[0].indexOf('@'); | match.index + | ||
match[0].indexOf('@'); | |||
decorations.push( | decorations.push( | ||
Decoration.mark({ | Decoration.mark({ | ||
class: 'preload-editor-syntax-directive' | class: | ||
'preload-editor-syntax-directive' | |||
}).range( | }).range( | ||
directiveStart, | directiveStart, | ||
directiveStart + match[1].length + 1 | directiveStart + | ||
match[1].length + | |||
1 | |||
) | ) | ||
); | ); | ||
const type = match[1].toLowerCase(); | const type = | ||
match[1].toLowerCase(); | |||
if (type !== 'if' && type !== 'elseif') continue; | if ( | ||
type !== 'if' && | |||
type !== 'elseif' | |||
) { | |||
continue; | |||
} | |||
const expression = | |||
match[2]; | |||
const expressionStart = | const expressionStart = | ||
match.index + match[0].indexOf(expression); | match.index + | ||
match[0].indexOf( | |||
expression | |||
); | |||
const expressionPattern = | |||
/[A-Za-z0-9_-]+/g; | |||
let expressionMatch; | let expressionMatch; | ||
while ( | while ( | ||
(expressionMatch = | ( | ||
expressionMatch = | |||
expressionPattern.exec( | |||
expression | |||
) | |||
) !== null | |||
) { | ) { | ||
const name = | const name = | ||
expressionMatch[0].toUpperCase(); | expressionMatch[0] | ||
.toUpperCase(); | |||
if (!knownVariables.has(name)) continue; | if ( | ||
!knownVariables.has(name) | |||
) { | |||
continue; | |||
} | |||
const start = | const start = | ||
expressionStart + expressionMatch.index; | expressionStart + | ||
expressionMatch.index; | |||
decorations.push( | decorations.push( | ||
| Line 960: | Line 1,267: | ||
}).range( | }).range( | ||
start, | start, | ||
start + expressionMatch[0].length | start + | ||
expressionMatch[0] | |||
.length | |||
) | ) | ||
); | ); | ||
| Line 966: | Line 1,275: | ||
} | } | ||
return Decoration.set(decorations, true); | return Decoration.set( | ||
decorations, | |||
true | |||
); | |||
}, | }, | ||
async installSyntaxHighlighting(view) { | async installSyntaxHighlighting(view) { | ||
await mw.loader.using('ext.CodeMirror.v6.lib'); | await mw.loader.using( | ||
'ext.CodeMirror.v6.lib' | |||
); | |||
const require = | |||
mw.loader.require; | |||
const { | const { | ||
StateField, | StateField, | ||
| Line 978: | Line 1,294: | ||
EditorView, | EditorView, | ||
Decoration | Decoration | ||
} = require('ext.CodeMirror.v6.lib'); | } = 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({ | view.dispatch({ | ||
effects: StateEffect.appendConfig.of(syntaxField) | effects: | ||
StateEffect.appendConfig.of( | |||
syntaxField | |||
) | |||
}); | }); | ||
this.editor.syntaxField = syntaxField; | this.editor.syntaxField = | ||
syntaxField; | |||
}, | }, | ||
| Line 1,006: | Line 1,337: | ||
async mountCodeMirror(textarea) { | async mountCodeMirror(textarea) { | ||
return new Promise((resolve, reject) => { | return new Promise( | ||
(resolve, reject) => { | |||
let settled = false; | |||
const cleanup = () => { | |||
mw.hook( | |||
'dev.CCM.ready' | |||
).remove(onReady); | |||
mw.hook( | |||
'dev.CCM.load' | |||
).remove(onLoad); | |||
}; | |||
settled = | 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.state.dirty = | |||
true; | |||
this.reanalyze(); | |||
this.renderVariablesBySelector(); | |||
this.renderAnalysisBySelector(); | |||
this.renderConfigPreviewBySelector(); | |||
this.updateEditorTools(); | |||
}; | |||
view.dom.addEventListener( | |||
view.dom. | 'input', | ||
' | inputHandler | ||
); | ); | ||
view.dom.classList.add( | |||
'preload-editor-codemirror' | |||
'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 | |||
); | |||
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); | |||
} | |||
); | |||
}, | }, | ||
| Line 1,101: | Line 1,458: | ||
init() { | init() { | ||
const root = $('#mw-content-text'); | const root = | ||
$('#mw-content-text'); | |||
if (!root.length) return; | if (!root.length) return; | ||
this.root = $('<div>') | this.root = $('<div>') | ||
.attr('id', 'preload-editor') | .attr( | ||
.addClass('preload-editor'); | 'id', | ||
'preload-editor' | |||
) | |||
.addClass( | |||
'preload-editor' | |||
); | |||
root.append(this.root); | root.append(this.root); | ||
this.renderShell(); | this.renderShell(); | ||
}, | }, | ||
| Line 1,116: | Line 1,481: | ||
this.root.empty(); | this.root.empty(); | ||
const loader = this.createSection('Preload'); | const loader = | ||
this.createSection( | |||
'Preload' | |||
); | |||
const titleInput = this.textInput( | const titleInput = | ||
this.textInput( | |||
this.state.preloadTitle, | |||
'Template:Example/Preload' | |||
).attr( | |||
'id', | |||
'preload-editor-title-input' | |||
); | |||
const loadButton = this.createButton( | const loadButton = | ||
this.createButton( | |||
'Load', | |||
'preload-editor-button-primary', | |||
() => | |||
this.loadExisting( | |||
const newButton = this.createButton( | titleInput.val() | ||
) | |||
); | |||
const newButton = | |||
this.createButton( | |||
'New', | |||
'', | |||
() => this.startNew() | |||
); | |||
const sampleButton = this.createButton( | const sampleButton = | ||
this.createButton( | |||
'Load Sample Page', | |||
'', | |||
() => | |||
this.loadSamplePage() | |||
); | |||
loader.append( | loader.append( | ||
$('<div>') | $('<div>') | ||
.addClass('preload-editor-toolbar') | .addClass( | ||
'preload-editor-toolbar' | |||
) | |||
.append( | .append( | ||
titleInput, | titleInput, | ||
| Line 1,154: | Line 1,535: | ||
this.root.append(loader); | this.root.append(loader); | ||
this.status = $('<div>') | this.status = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-status' | |||
); | |||
this.root.append( | |||
this.status | |||
); | |||
this. | this.workspace = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-workspace' | |||
); | |||
this. | this.root.append( | ||
. | this.workspace | ||
); | |||
this.renderWelcome(); | this.renderWelcome(); | ||
}, | }, | ||
| Line 1,169: | Line 1,561: | ||
this.workspace.empty(); | this.workspace.empty(); | ||
const section = this.createSection('Get started'); | const section = | ||
this.createSection( | |||
'Get started' | |||
); | |||
$('<p>') | $('<p>') | ||
| Line 1,177: | Line 1,572: | ||
.appendTo(section); | .appendTo(section); | ||
const tips = $('<ul>') | const tips = | ||
$('<ul>') | |||
.addClass( | |||
'preload-editor-tips' | |||
); | |||
[ | [ | ||
| Line 1,185: | Line 1,583: | ||
'Sample page: loads normal wiki page content so you can turn it into a preload.' | 'Sample page: loads normal wiki page content so you can turn it into a preload.' | ||
].forEach(text => { | ].forEach(text => { | ||
$('<li>').text(text).appendTo(tips); | $('<li>') | ||
.text(text) | |||
.appendTo(tips); | |||
}); | }); | ||
| Line 1,202: | Line 1,602: | ||
); | ); | ||
if (type) this.status.addClass('is-' + type); | if (type) { | ||
this.status.text(message || ''); | this.status.addClass( | ||
'is-' + type | |||
); | |||
} | |||
this.status.text( | |||
message || '' | |||
); | |||
}, | }, | ||
| Line 1,209: | Line 1,616: | ||
async loadExisting(title) { | async loadExisting(title) { | ||
title = String(title || '').trim(); | title = | ||
String(title || '').trim(); | |||
if (!title) { | if (!title) { | ||
| Line 1,216: | Line 1,624: | ||
{ type: 'warn' } | { type: 'warn' } | ||
); | ); | ||
return; | return; | ||
} | } | ||
this.setStatus('Loading preload...', 'loading'); | this.setStatus( | ||
'Loading preload...', | |||
'loading' | |||
); | |||
try { | try { | ||
const page = await this.getPage(title); | const page = | ||
await this.getPage(title); | |||
if (!page) { | if (!page) { | ||
const suggestions = | const suggestions = | ||
await this.findSimilarPreloads(title); | await this.findSimilarPreloads( | ||
title | |||
); | |||
this.renderMissingPage( | |||
title, | |||
suggestions | |||
); | |||
this.setStatus( | this.setStatus( | ||
'Preload not found.', | 'Preload not found.', | ||
'warning' | 'warning' | ||
); | ); | ||
return; | return; | ||
} | } | ||
| Line 1,246: | Line 1,666: | ||
); | ); | ||
} catch (error) { | } catch (error) { | ||
console.error('Preload Editor:', error); | console.error( | ||
'Preload Editor:', | |||
error | |||
); | |||
this.setStatus( | this.setStatus( | ||
error.message || 'Could not load preload.', | error.message || | ||
'Could not load preload.', | |||
'error' | 'error' | ||
); | ); | ||
| Line 1,255: | Line 1,680: | ||
async findSimilarPreloads(title) { | async findSimilarPreloads(title) { | ||
let query = title | let query = | ||
title | |||
.replace( | |||
/\/Preload$/i, | |||
'' | |||
) | |||
.trim(); | |||
if (!query) query = title; | if (!query) { | ||
query = title; | |||
} | |||
try { | try { | ||
const results = await this.searchPages(query); | const results = | ||
await this.searchPages( | |||
query | |||
); | |||
return results.filter(result => | return results.filter( | ||
result => | |||
/\/Preload$/i.test( | |||
result.title | |||
) | |||
); | ); | ||
} catch (error) { | } catch (error) { | ||
| Line 1,272: | Line 1,709: | ||
error | error | ||
); | ); | ||
return []; | return []; | ||
} | } | ||
}, | }, | ||
renderMissingPage(title, suggestions) { | renderMissingPage( | ||
title, | |||
suggestions | |||
) { | |||
this.workspace.empty(); | this.workspace.empty(); | ||
const section = this.createSection('Preload not found'); | const section = | ||
this.createSection( | |||
'Preload not found' | |||
); | |||
$('<p>') | $('<p>') | ||
.text('No preload exists at "' + title + '".') | .text( | ||
'No preload exists at "' + | |||
title + | |||
'".' | |||
) | |||
.appendTo(section); | .appendTo(section); | ||
| Line 1,290: | Line 1,738: | ||
.appendTo(section); | .appendTo(section); | ||
const list = $('<div>') | const list = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-suggestions' | |||
); | |||
suggestions.forEach(result => { | suggestions.forEach( | ||
result => { | |||
list.append( | |||
this.createButton( | |||
result.title, | |||
'preload-editor-suggestion', | |||
) | () => | ||
this.loadExisting( | |||
result.title | |||
) | |||
) | |||
); | |||
} | |||
); | |||
section.append(list); | section.append(list); | ||
| Line 1,310: | Line 1,766: | ||
'Create this preload', | 'Create this preload', | ||
'preload-editor-button-primary', | 'preload-editor-button-primary', | ||
() => this.startNew(title) | () => | ||
this.startNew(title) | |||
) | ) | ||
); | ); | ||
| Line 1,318: | Line 1,775: | ||
async startNew(title) { | async startNew(title) { | ||
title = String(title || '').trim(); | title = | ||
String(title || '').trim(); | |||
this.state = { | this.state = { | ||
preloadTitle: title, | preloadTitle: title, | ||
preloadSource: '', | preloadSource: '', | ||
preloadContent: this.defaults.preload, | preloadContent: | ||
config: this.emptyConfig(), | this.defaults.preload, | ||
config: | |||
this.emptyConfig(), | |||
variables: ['PAGE'], | variables: ['PAGE'], | ||
conditions: [], | conditions: [], | ||
| Line 1,340: | Line 1,800: | ||
async loadSamplePage() { | async loadSamplePage() { | ||
const result = await this.createPromptDialog( | 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; | if (!result) return; | ||
| Line 1,353: | Line 1,814: | ||
try { | try { | ||
const page = await this.getPage(result); | const page = | ||
await this.getPage(result); | |||
if (!page) { | if (!page) { | ||
throw new Error( | throw new Error( | ||
'Sample page not found: ' + result | 'Sample page not found: ' + | ||
result | |||
); | ); | ||
} | } | ||
const sample = | const sample = | ||
this.convertSampleToPreload(page.content); | this.convertSampleToPreload( | ||
page.content | |||
); | |||
this.state = { | this.state = { | ||
| Line 1,368: | Line 1,833: | ||
preloadSource: '', | preloadSource: '', | ||
preloadContent: sample, | preloadContent: sample, | ||
config: this.emptyConfig(), | config: | ||
this.emptyConfig(), | |||
variables: ['PAGE'], | variables: ['PAGE'], | ||
conditions: [], | conditions: [], | ||
| Line 1,397: | Line 1,863: | ||
convertSampleToPreload(source) { | convertSampleToPreload(source) { | ||
let content = String(source || '').trim(); | let content = | ||
String(source || '').trim(); | |||
const include = content.match( | const include = | ||
content.match( | |||
/<includeonly>([\s\S]*?)<\/includeonly>/i | |||
); | |||
if (include) content = include[1].trim(); | if (include) { | ||
content = | |||
include[1].trim(); | |||
} | |||
content = content | content = | ||
content | |||
.replace( | |||
/<noinclude>[\s\S]*?<\/noinclude>/gi, | |||
'' | |||
) | |||
.trim(); | |||
return ( | return ( | ||
| Line 1,420: | Line 1,892: | ||
}, | }, | ||
createPromptDialog(title, message) { | createPromptDialog( | ||
return new Promise(resolve => { | 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' | |||
); | |||
} | |||
} | } | ||
); | |||
} | |||
); | |||
}, | |||
async loadDefinition( | |||
title, | |||
source | |||
) { | |||
const content = | |||
this.extractIncludeonly( | |||
source | |||
); | ); | ||
let config = | |||
this.emptyConfig(); | |||
let config = this.emptyConfig(); | |||
const configTitle = | const configTitle = | ||
| Line 1,495: | Line 2,014: | ||
const configPage = | const configPage = | ||
await this.getPage(configTitle); | await this.getPage( | ||
configTitle | |||
); | |||
if (configPage) { | if (configPage) { | ||
try { | try { | ||
config = JSON.parse(configPage.content); | config = | ||
JSON.parse( | |||
configPage.content | |||
); | |||
} catch (error) { | } catch (error) { | ||
throw new Error( | throw new Error( | ||
| Line 1,509: | Line 2,033: | ||
const analysis = | const analysis = | ||
this.analyzePreload(content); | this.analyzePreload( | ||
content | |||
); | |||
config = this.prepareConfigForVariables( | config = | ||
this.prepareConfigForVariables( | |||
config, | |||
analysis | |||
); | |||
this.state = { | this.state = { | ||
| Line 1,521: | Line 2,048: | ||
preloadContent: source, | preloadContent: source, | ||
config, | config, | ||
variables: analysis.variables, | variables: | ||
conditions: analysis.conditions, | analysis.variables, | ||
conditions: | |||
analysis.conditions, | |||
mode: 'edit', | mode: 'edit', | ||
dirty: false | dirty: false | ||
| Line 1,536: | Line 2,065: | ||
this.workspace.empty(); | this.workspace.empty(); | ||
const layout = $('<div>') | const layout = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-layout' | |||
); | |||
const left = $('<div>') | const left = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-left' | |||
); | |||
const right = $('<div>') | const right = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-right' | |||
); | |||
const editorSection = this.createSection( | const editorSection = | ||
this.createSection( | |||
'Preload', | |||
'preload-editor-main-section' | |||
); | |||
const meta = $('<div>') | const meta = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-meta' | |||
); | |||
$('<strong>') | $('<strong>') | ||
.text(this.state.preloadTitle || 'New preload') | .text( | ||
this.state.preloadTitle || | |||
'New preload' | |||
) | |||
.appendTo(meta); | .appendTo(meta); | ||
if (this.state.mode === 'new') { | if ( | ||
this.state.mode === | |||
'new' | |||
) { | |||
$('<span>') | $('<span>') | ||
.addClass('preload-editor-badge') | .addClass( | ||
'preload-editor-badge' | |||
) | |||
.text('NEW') | .text('NEW') | ||
.appendTo(meta); | .appendTo(meta); | ||
| Line 1,570: | Line 2,120: | ||
); | ); | ||
const textarea = this.textarea( | const textarea = | ||
this.textarea( | |||
this.state.preloadContent | |||
) | |||
.attr( | |||
'id', | |||
'preload-editor-content' | |||
) | |||
.addClass( | |||
'preload-editor-content' | |||
); | |||
const editorMount = $('<div>') | const editorMount = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-codemirror-mount' | |||
) | |||
.append(textarea); | |||
editorSection.append( | |||
editorMount | |||
); | |||
left.append(editorSection); | left.append(editorSection); | ||
const analysisSection = | const analysisSection = | ||
this.createSection('Analysis'); | this.createSection( | ||
'Analysis' | |||
); | |||
const analysisContainer = $('<div>') | const analysisContainer = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-analysis' | |||
); | |||
analysisSection.append(analysisContainer); | analysisSection.append( | ||
analysisContainer | |||
); | |||
const variablesSection = | const variablesSection = | ||
this.createSection('Variables'); | this.createSection( | ||
'Variables' | |||
); | |||
const variableContainer = $('<div>') | const variableContainer = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-variables' | |||
); | |||
variablesSection.append(variableContainer); | variablesSection.append( | ||
variableContainer | |||
); | |||
const configSection = | const configSection = | ||
this.createSection('Generated configuration'); | this.createSection( | ||
'Generated configuration' | |||
); | |||
const configPreview = $('<pre>') | const configPreview = | ||
$('<pre>') | |||
.addClass( | |||
'preload-editor-config-preview' | |||
); | |||
configSection.append(configPreview); | configSection.append( | ||
configPreview | |||
); | |||
const actions = $('<div>') | const actions = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-actions' | |||
); | |||
actions.append( | actions.append( | ||
| Line 1,630: | Line 2,216: | ||
); | ); | ||
layout.append(left, right); | layout.append( | ||
this.workspace.append(layout); | left, | ||
right | |||
); | |||
this.workspace.append( | |||
layout | |||
); | |||
this.reanalyze(); | this.reanalyze(); | ||
this.renderVariables(variableContainer); | |||
this.renderAnalysis(analysisContainer); | this.renderVariables( | ||
this.renderConfigPreview(configPreview); | variableContainer | ||
); | |||
this.renderAnalysis( | |||
analysisContainer | |||
); | |||
this.renderConfigPreview( | |||
configPreview | |||
); | |||
this.updateEditorTools(); | this.updateEditorTools(); | ||
this.mountCodeMirror(textarea.get(0)).catch(error => { | this.mountCodeMirror( | ||
textarea.get(0) | |||
).catch(error => { | |||
console.error( | console.error( | ||
'Preload Editor CodeMirror:', | 'Preload Editor CodeMirror:', | ||
| Line 1,654: | Line 2,258: | ||
reanalyze() { | reanalyze() { | ||
const source = this.getEditorContent(); | const source = | ||
const content = this.extractIncludeonly(source | this.getEditorContent(); | ||
const content = | |||
this.extractIncludeonly( | |||
source | |||
); | |||
this.state.preloadContent = source; | const analysis = | ||
this.state.variables = analysis.variables; | this.analyzePreload( | ||
this.state.conditions = analysis.conditions; | content | ||
); | |||
this.state.preloadContent = | |||
source; | |||
this.state.variables = | |||
analysis.variables; | |||
this.state.conditions = | |||
analysis.conditions; | |||
this.state.config = | this.state.config = | ||
| Line 1,669: | Line 2,287: | ||
); | ); | ||
this.analysis = analysis; | this.analysis = | ||
analysis; | |||
}, | }, | ||
| Line 1,683: | Line 2,302: | ||
); | ); | ||
const variableCount = analysis.variables | const variableCount = | ||
analysis.variables.length; | |||
const | const conditionCount = | ||
. | analysis.conditions.length; | ||
summary.append( | const summary = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-analysis-summary' | |||
); | |||
summary.append( | |||
$('<span>').text( | $('<span>').text( | ||
variableCount + | variableCount + | ||
' variable' + | ' variable' + | ||
(variableCount === 1 ? '' : 's') | ( | ||
variableCount === 1 | |||
? '' | |||
: 's' | |||
) | |||
), | ), | ||
$('<span>').text( | $('<span>').text( | ||
conditionCount + | conditionCount + | ||
' condition marker' + | ' condition marker' + | ||
(conditionCount === 1 ? '' : 's') | ( | ||
conditionCount === 1 | |||
? '' | |||
: 's' | |||
) | |||
) | ) | ||
); | ); | ||
container.append(summary); | container.append( | ||
summary | |||
); | |||
if (analysis.conditionVariables.length) { | if ( | ||
analysis.conditionVariables.length | |||
) { | |||
container.append( | container.append( | ||
$('<div>') | $('<div>') | ||
| Line 1,712: | Line 2,349: | ||
.text( | .text( | ||
'Condition variables: ' + | 'Condition variables: ' + | ||
analysis.conditionVariables.join(', ') | analysis.conditionVariables.join( | ||
', ' | |||
) | |||
) | ) | ||
); | ); | ||
} | } | ||
const validation = this.validateConfig( | const validation = | ||
this.validateConfig( | |||
this.state.config, | |||
analysis | |||
); | |||
if (validation.errors.length) { | if ( | ||
const errors = $('<ul>') | validation.errors.length | ||
) { | |||
const errors = | |||
$('<ul>') | |||
.addClass( | |||
'preload-editor-validation-errors' | |||
); | |||
validation.errors.forEach(error => { | validation.errors.forEach( | ||
error => { | |||
$('<li>') | |||
.text(error) | |||
.appendTo(errors); | |||
} | |||
); | |||
container.append(errors); | container.append( | ||
errors | |||
); | |||
} | } | ||
if (validation.warnings.length) { | if ( | ||
const warnings = $('<ul>') | validation.warnings.length | ||
) { | |||
const warnings = | |||
$('<ul>') | |||
.addClass( | |||
'preload-editor-validation-warnings' | |||
); | |||
validation.warnings.forEach(warning => { | validation.warnings.forEach( | ||
warning => { | |||
$('<li>') | |||
.text(warning) | |||
.appendTo(warnings); | |||
} | |||
); | |||
container.append(warnings); | container.append( | ||
warnings | |||
); | |||
} | } | ||
if (!validation.errors.length && | if ( | ||
!validation.warnings.length) { | !validation.errors.length && | ||
!validation.warnings.length | |||
) { | |||
container.append( | container.append( | ||
$('<div>') | $('<div>') | ||
| Line 1,764: | Line 2,424: | ||
renderAnalysisBySelector() { | renderAnalysisBySelector() { | ||
const container = | const container = | ||
this.root | this.root && | ||
this.root.find( | |||
'.preload-editor-analysis' | '.preload-editor-analysis' | ||
); | ); | ||
if (container | if ( | ||
this.renderAnalysis(container); | container && | ||
container.length | |||
) { | |||
this.renderAnalysis( | |||
container | |||
); | |||
} | } | ||
}, | }, | ||
| Line 1,784: | Line 2,450: | ||
); | ); | ||
const variables = analysis.variables.slice(); | const variables = | ||
analysis.variables.slice(); | |||
const ordered = ['PAGE']; | const ordered = ['PAGE']; | ||
variables.forEach(name => { | variables.forEach(name => { | ||
if (name !== 'PAGE' && | if ( | ||
!ordered.includes(name)) { | name !== 'PAGE' && | ||
!ordered.includes(name) | |||
) { | |||
ordered.push(name); | ordered.push(name); | ||
} | } | ||
| Line 1,797: | Line 2,467: | ||
this.state.config.fields || {} | this.state.config.fields || {} | ||
).forEach(name => { | ).forEach(name => { | ||
const upper = name.toUpperCase(); | const upper = | ||
name.toUpperCase(); | |||
if (!ordered.includes(upper)) { | if ( | ||
!ordered.includes(upper) | |||
) { | |||
ordered.push(upper); | ordered.push(upper); | ||
} | } | ||
| Line 1,807: | Line 2,480: | ||
const field = | const field = | ||
this.state.config.fields[name] || | this.state.config.fields[name] || | ||
this.createDefaultField(name, analysis); | this.createDefaultField( | ||
name, | |||
analysis | |||
); | |||
this.state.config.fields[name] = field; | this.state.config.fields[name] = | ||
field; | |||
container.append( | container.append( | ||
| Line 1,823: | Line 2,500: | ||
renderVariablesBySelector() { | renderVariablesBySelector() { | ||
const container = | const container = | ||
this.root | this.root && | ||
this.root.find( | |||
'.preload-editor-variables' | '.preload-editor-variables' | ||
); | ); | ||
if (container | if ( | ||
this.renderVariables(container); | container && | ||
container.length | |||
) { | |||
this.renderVariables( | |||
container | |||
); | |||
} | } | ||
}, | }, | ||
createVariableCard(name, field, analysis) { | createVariableCard( | ||
const card = $('<div>') | name, | ||
field, | |||
analysis | |||
) { | |||
const card = | |||
$('<div>') | |||
.addClass( | |||
'preload-editor-variable' | |||
) | |||
.attr( | |||
'data-variable', | |||
name | |||
); | |||
const header = $('<div>') | const header = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-variable-header' | |||
); | |||
header.append($('<h3>').text(name)); | header.append( | ||
$('<h3>').text(name) | |||
); | |||
if (name === 'PAGE') { | if (name === 'PAGE') { | ||
header.append( | header.append( | ||
$('<span>') | $('<span>') | ||
.addClass('preload-editor-badge') | .addClass( | ||
'preload-editor-badge' | |||
) | |||
.text('AUTOMATIC') | .text('AUTOMATIC') | ||
); | ); | ||
} else if (!analysis.variables.includes(name)) { | } else if ( | ||
!analysis.variables.includes(name) | |||
) { | |||
header.append( | header.append( | ||
$('<span>') | $('<span>') | ||
| Line 1,860: | Line 2,562: | ||
card.append(header); | card.append(header); | ||
const label = this.textInput(field.label || ''); | const label = | ||
this.textInput( | |||
field.label || '' | |||
); | |||
label.on( | |||
'input.preloadEditor', | |||
() => { | |||
field.label = | |||
label.val(); | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | |||
this.fieldRow( | |||
'Label', | |||
label | |||
) | |||
); | |||
const description = | |||
this.textarea( | |||
field.description || '' | |||
).addClass( | |||
'preload-editor-small-textarea' | |||
); | |||
description.on( | |||
field.description | 'input.preloadEditor', | ||
() => { | |||
field.description = | |||
description.val(); | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 1,893: | Line 2,616: | ||
['boolean', 'Boolean'], | ['boolean', 'Boolean'], | ||
['select', 'Select'], | ['select', 'Select'], | ||
['multiselect', 'Multi-select'], | [ | ||
'multiselect', | |||
'Multi-select' | |||
], | |||
['page', 'Page'], | ['page', 'Page'], | ||
['user', 'User'], | ['user', 'User'], | ||
| Line 1,899: | Line 2,625: | ||
]; | ]; | ||
const typeSelect = this.select( | const typeSelect = | ||
this.select( | |||
types.map( | |||
([value, label]) => ({ | |||
value, | |||
label | |||
}) | |||
), | |||
field.type || 'text' | |||
); | |||
typeSelect.on('change.preloadEditor', () => { | typeSelect.on( | ||
'change.preloadEditor', | |||
() => { | |||
field.type = | |||
typeSelect.val(); | |||
this.markDirty(); | |||
card.replaceWith( | |||
this.createVariableCard( | |||
name, | |||
field, | |||
analysis | |||
) | |||
); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 1,932: | Line 2,666: | ||
field.type = 'page'; | field.type = 'page'; | ||
const required = this.checkbox( | const required = | ||
this.checkbox( | |||
field.required !== false | |||
); | |||
required.on('change.preloadEditor', () => { | required.on( | ||
'change.preloadEditor', | |||
() => { | |||
field.required = | |||
required.prop( | |||
'checked' | |||
); | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 1,951: | Line 2,691: | ||
); | ); | ||
const namespace = this.select( | const namespace = | ||
this.select( | |||
[ | |||
{ | |||
value: 'Main', | |||
label: 'Main' | |||
}, | |||
{ | |||
value: 'Template', | |||
); | label: 'Template' | ||
}, | |||
{ | |||
value: 'File', | |||
label: 'File' | |||
}, | |||
{ | |||
value: 'Category', | |||
label: 'Category' | |||
}, | |||
{ | |||
value: 'User', | |||
label: 'User' | |||
} | |||
], | |||
field.namespace || 'Main' | |||
); | |||
namespace.on( | |||
'change.preloadEditor', | |||
() => { | |||
field.namespace = | |||
namespace.val(); | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 1,975: | Line 2,736: | ||
); | ); | ||
const allowNew = this.checkbox( | const allowNew = | ||
this.checkbox( | |||
field['allow-new'] === true | |||
); | |||
allowNew.on('change.preloadEditor', () => { | allowNew.on( | ||
'change.preloadEditor', | |||
() => { | |||
field['allow-new'] = | |||
allowNew.prop( | |||
'checked' | |||
); | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 1,997: | Line 2,764: | ||
} | } | ||
const required = this.checkbox( | const required = | ||
this.checkbox( | |||
field.required === true | |||
); | |||
required.on('change.preloadEditor', () => { | required.on( | ||
'change.preloadEditor', | |||
() => { | |||
field.required = | |||
required.prop( | |||
'checked' | |||
); | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 2,016: | Line 2,789: | ||
); | ); | ||
const placeholder = this.textInput( | const placeholder = | ||
this.textInput( | |||
field.placeholder || '' | |||
); | |||
placeholder.on('input.preloadEditor', () => { | placeholder.on( | ||
'input.preloadEditor', | |||
() => { | |||
const value = | |||
placeholder.val(); | |||
if (value) { | |||
field.placeholder = | |||
value; | |||
} else { | |||
delete field.placeholder; | |||
} | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 2,037: | Line 2,819: | ||
); | ); | ||
const defaultInput = this.textInput( | const defaultInput = | ||
this.textInput( | |||
field.default === undefined | |||
? '' | |||
: field.default | |||
); | |||
defaultInput.on('input.preloadEditor', () => { | defaultInput.on( | ||
'input.preloadEditor', | |||
() => { | |||
const value = | |||
defaultInput.val(); | |||
if (value) { | |||
field.default = | |||
value; | |||
} else { | |||
delete field.default; | |||
} | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 2,061: | Line 2,852: | ||
); | ); | ||
if (field.type === 'textarea') { | if ( | ||
const maxRows = this.textInput( | 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.renderConfigPreviewBySelector(); | |||
} | } | ||
); | |||
card.append( | |||
this.fieldRow( | |||
card.append( | |||
this.fieldRow( | |||
'Maximum rows', | 'Maximum rows', | ||
maxRows | maxRows | ||
| Line 2,088: | Line 2,897: | ||
} | } | ||
if (field.type === 'select' || | if ( | ||
field.type === 'multiselect') { | field.type === 'select' || | ||
field.type === 'multiselect' | |||
) { | |||
this.renderSelectConfiguration( | this.renderSelectConfiguration( | ||
card, | card, | ||
| Line 2,099: | Line 2,910: | ||
}, | }, | ||
renderSelectConfiguration(card, field) { | renderSelectConfiguration( | ||
const source = this.textInput( | card, | ||
field | |||
) { | |||
const source = | |||
this.textInput( | |||
field.source || '', | |||
'category:Example' | |||
); | |||
source.on('input.preloadEditor', () => { | source.on( | ||
'input.preloadEditor', | |||
() => { | |||
const value = | |||
source.val(); | |||
if (value) { | |||
field.source = value; | |||
} else { | |||
delete field.source; | |||
} | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | |||
); | |||
card.append( | card.append( | ||
| Line 2,123: | Line 2,945: | ||
); | ); | ||
const sourcePrefix = this.textInput( | const sourcePrefix = | ||
this.textInput( | |||
field['source-prefix'] || '' | |||
); | |||
sourcePrefix.on('input.preloadEditor', () => { | sourcePrefix.on( | ||
'input.preloadEditor', | |||
() => { | |||
const value = | |||
sourcePrefix.val(); | |||
if (value) { | |||
field['source-prefix'] = | |||
value; | |||
} else { | |||
delete field[ | |||
'source-prefix' | |||
]; | |||
} | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
} | } | ||
); | |||
card.append( | card.append( | ||
| Line 2,148: | Line 2,978: | ||
); | ); | ||
const excludeCategory = this.textInput( | const excludeCategory = | ||
this.textInput( | |||
field.exclude && | |||
field.exclude.category || | |||
'' | |||
); | |||
excludeCategory.on('input.preloadEditor', () => { | 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.renderConfigPreviewBySelector(); | |||
} | } | ||
); | |||
card.append( | card.append( | ||
| Line 2,195: | Line 3,042: | ||
const optionsValue = | const optionsValue = | ||
Array.isArray(field.options) | Array.isArray(field.options) | ||
? field.options.map(option => { | ? 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 | const options = | ||
.addClass('preload-editor-small-textarea'); | 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.renderConfigPreviewBySelector(); | |||
} | } | ||
); | |||
card.append( | card.append( | ||
| Line 2,253: | Line 3,131: | ||
}, | }, | ||
createTransformControls(field, property, title) { | createTransformControls( | ||
const wrapper = $('<div>') | field, | ||
property, | |||
title | |||
) { | |||
const wrapper = | |||
$('<div>') | |||
.addClass( | |||
'preload-editor-transform' | |||
); | |||
$('<h4>') | $('<h4>') | ||
.text(title) | .text(title) | ||
| Line 2,263: | Line 3,148: | ||
const existing = | const existing = | ||
field[property] && | field[property] && | ||
typeof field[property] === 'object' | typeof field[property] === | ||
'object' | |||
? field[property] | ? field[property] | ||
: {}; | : {}; | ||
const transform = this.select( | 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( | const prefix = | ||
this.textInput( | |||
existing.prefix || '' | |||
); | |||
const suffix = this.textInput( | const suffix = | ||
this.textInput( | |||
existing.suffix || '' | |||
); | |||
const from = this.textInput( | const from = | ||
this.textInput( | |||
existing.from || '' | |||
); | |||
const to = this.textInput( | const to = | ||
this.textInput( | |||
existing.to || '' | |||
); | |||
const controls = $('<div>') | const controls = | ||
$('<div>') | |||
.addClass( | |||
'preload-editor-transform-controls' | |||
); | |||
const prefixRow = | const prefixRow = | ||
this.fieldRow('Prefix', prefix); | this.fieldRow( | ||
'Prefix', | |||
prefix | |||
); | |||
const suffixRow = | const suffixRow = | ||
this.fieldRow('Suffix', suffix); | this.fieldRow( | ||
'Suffix', | |||
suffix | |||
); | |||
const fromRow = | const fromRow = | ||
this.fieldRow('From', from); | this.fieldRow( | ||
'From', | |||
from | |||
); | |||
const toRow = | const toRow = | ||
this.fieldRow('To', to); | this.fieldRow( | ||
'To', | |||
to | |||
); | |||
const renderInputs = () => { | 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(); | |||
transform | |||
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() | |||
}; | |||
} | |||
transform.on( | if (object) { | ||
field[property] = | |||
object; | |||
} else { | |||
delete field[property]; | |||
} | |||
this.markDirty(); | |||
this.renderConfigPreviewBySelector(); | |||
}; | |||
transform.on( | |||
'change.preloadEditor', | 'change.preloadEditor', | ||
() => { | () => { | ||
| Line 2,380: | Line 3,312: | ||
); | ); | ||
[prefix, suffix, from, to].forEach(input => { | [ | ||
prefix, | |||
suffix, | |||
from, | |||
to | |||
].forEach(input => { | |||
input.on( | input.on( | ||
'input.preloadEditor', | 'input.preloadEditor', | ||
| Line 2,399: | Line 3,336: | ||
renderInputs(); | renderInputs(); | ||
wrapper.append(controls); | |||
wrapper.append( | |||
controls | |||
); | |||
return wrapper; | return wrapper; | ||
| Line 2,407: | Line 3,347: | ||
getCleanConfig() { | getCleanConfig() { | ||
const config = JSON.parse( | const config = | ||
JSON.parse( | |||
JSON.stringify( | |||
this.state.config | |||
) | |||
); | |||
const fields = | |||
config.fields || {}; | |||
const orderedFields = {}; | const orderedFields = {}; | ||
if (fields.PAGE) { | if (fields.PAGE) { | ||
orderedFields.PAGE = fields.PAGE; | orderedFields.PAGE = | ||
fields.PAGE; | |||
} | } | ||
| Line 2,421: | Line 3,367: | ||
.filter( | .filter( | ||
name => | name => | ||
name.toUpperCase() !== 'PAGE' | name.toUpperCase() !== | ||
'PAGE' | |||
) | ) | ||
.forEach(name => { | .forEach(name => { | ||
orderedFields[name] = fields[name]; | orderedFields[name] = | ||
fields[name]; | |||
}); | }); | ||
config.fields = orderedFields; | config.fields = | ||
orderedFields; | |||
return config; | return config; | ||
}, | }, | ||
| Line 2,441: | Line 3,391: | ||
renderConfigPreview(container) { | renderConfigPreview(container) { | ||
if (container) { | if (container) { | ||
container.text(this.getConfigJSON()); | container.text( | ||
this.getConfigJSON() | |||
); | |||
} | } | ||
}, | }, | ||
| Line 2,447: | Line 3,399: | ||
renderConfigPreviewBySelector() { | renderConfigPreviewBySelector() { | ||
const preview = | const preview = | ||
this.root | this.root && | ||
this.root.find( | |||
'.preload-editor-config-preview' | '.preload-editor-config-preview' | ||
); | ); | ||
if (preview | if ( | ||
this.renderConfigPreview(preview); | preview && | ||
preview.length | |||
) { | |||
this.renderConfigPreview( | |||
preview | |||
); | |||
} | } | ||
| Line 2,467: | Line 3,425: | ||
let title = | let title = | ||
String( | String( | ||
this.state.preloadTitle || '' | this.state.preloadTitle || | ||
'' | |||
).trim(); | ).trim(); | ||
| Line 2,480: | Line 3,439: | ||
title = result; | title = result; | ||
this.state.preloadTitle = title; | this.state.preloadTitle = | ||
title; | |||
} | } | ||
| Line 2,489: | Line 3,449: | ||
this.validateBeforeSave(); | this.validateBeforeSave(); | ||
if (validation.errors.length) { | if ( | ||
this.showValidationErrors(validation); | validation.errors.length | ||
) { | |||
this.showValidationErrors( | |||
validation | |||
); | |||
return; | return; | ||
} | } | ||
| Line 2,501: | Line 3,466: | ||
try { | try { | ||
const exists = | const exists = | ||
await this.pageExists(title); | await this.pageExists( | ||
title | |||
); | |||
await this.savePage( | await this.savePage( | ||
| Line 2,512: | Line 3,479: | ||
); | ); | ||
this.state.mode = 'edit'; | this.state.mode = | ||
this.state.dirty = false; | 'edit'; | ||
this.state.dirty = | |||
false; | |||
this.setStatus( | this.setStatus( | ||
| Line 2,522: | Line 3,492: | ||
mw.notify( | mw.notify( | ||
'Preload saved.', | 'Preload saved.', | ||
{ type: 'success' } | { | ||
type: 'success' | |||
} | |||
); | ); | ||
} catch (error) { | } catch (error) { | ||
| Line 2,539: | Line 3,511: | ||
error.message || | error.message || | ||
'Could not save preload.', | 'Could not save preload.', | ||
{ type: 'error' } | { | ||
type: 'error' | |||
} | |||
); | ); | ||
} | } | ||
| Line 2,547: | Line 3,521: | ||
let title = | let title = | ||
String( | String( | ||
this.state.preloadTitle || '' | this.state.preloadTitle || | ||
'' | |||
).trim(); | ).trim(); | ||
| Line 2,560: | Line 3,535: | ||
title = result; | title = result; | ||
this.state.preloadTitle = title; | this.state.preloadTitle = | ||
title; | |||
} | } | ||
| Line 2,569: | Line 3,545: | ||
this.validateBeforeSave(); | this.validateBeforeSave(); | ||
if (validation.errors.length) { | if ( | ||
this.showValidationErrors(validation); | validation.errors.length | ||
) { | |||
this.showValidationErrors( | |||
validation | |||
); | |||
return; | return; | ||
} | } | ||
const configTitle = | const configTitle = | ||
title.replace(/\/+$/, '') + | title.replace( | ||
/\/+$/, | |||
'' | |||
) + | |||
'/config.json'; | '/config.json'; | ||
| Line 2,585: | Line 3,569: | ||
try { | try { | ||
const preloadExists = | const preloadExists = | ||
await this.pageExists(title); | await this.pageExists( | ||
title | |||
); | |||
await this.savePage( | await this.savePage( | ||
| Line 2,597: | Line 3,583: | ||
const configExists = | const configExists = | ||
await this.pageExists(configTitle); | await this.pageExists( | ||
configTitle | |||
); | |||
await this.savePage( | await this.savePage( | ||
| Line 2,608: | Line 3,596: | ||
); | ); | ||
this.state.mode = 'edit'; | this.state.mode = | ||
this.state.dirty = false; | 'edit'; | ||
this.state.preloadTitle = title; | |||
this.state.dirty = | |||
false; | |||
this.state.preloadTitle = | |||
title; | |||
this.setStatus( | this.setStatus( | ||
| Line 2,619: | Line 3,612: | ||
mw.notify( | mw.notify( | ||
'Preload and config saved.', | 'Preload and config saved.', | ||
{ type: 'success' } | { | ||
type: 'success' | |||
} | |||
); | ); | ||
} catch (error) { | } catch (error) { | ||
| Line 2,636: | Line 3,631: | ||
error.message || | error.message || | ||
'Could not save preload/config.', | 'Could not save preload/config.', | ||
{ type: 'error' } | { | ||
type: 'error' | |||
} | |||
); | ); | ||
} | } | ||
| Line 2,647: | Line 3,644: | ||
const title = | const title = | ||
String( | String( | ||
this.state.preloadTitle || '' | this.state.preloadTitle || | ||
'' | |||
).trim(); | ).trim(); | ||
| Line 2,668: | Line 3,666: | ||
this.getEditorContent(); | this.getEditorContent(); | ||
this.state.preloadContent = content; | this.state.preloadContent = | ||
content; | |||
if (!content.trim()) { | if (!content.trim()) { | ||
| Line 2,676: | Line 3,675: | ||
} | } | ||
if (!/<includeonly>/i.test(content)) { | if ( | ||
!/<includeonly>/i.test( | |||
content | |||
) | |||
) { | |||
warnings.push( | warnings.push( | ||
'The preload does not contain an <includeonly> section.' | 'The preload does not contain an <includeonly> section.' | ||
| Line 2,682: | Line 3,685: | ||
} | } | ||
if (!/<noinclude>/i.test(content)) { | if ( | ||
!/<noinclude>/i.test( | |||
content | |||
) | |||
) { | |||
warnings.push( | warnings.push( | ||
'The preload does not contain a <noinclude> section.' | 'The preload does not contain a <noinclude> section.' | ||
| Line 2,690: | Line 3,697: | ||
const analysis = | const analysis = | ||
this.analyzePreload( | this.analyzePreload( | ||
this.extractIncludeonly(content) | this.extractIncludeonly( | ||
content | |||
) | |||
); | ); | ||
| Line 2,699: | Line 3,708: | ||
); | ); | ||
errors.push(...configValidation.errors); | errors.push( | ||
warnings.push(...configValidation.warnings); | ...configValidation.errors | ||
); | |||
warnings.push( | |||
...configValidation.warnings | |||
); | |||
let depth = 0; | let depth = 0; | ||
analysis.conditions.forEach(condition => { | 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) { | if (depth > 0) { | ||
| Line 2,739: | Line 3,764: | ||
errors, | errors, | ||
warnings, | warnings, | ||
valid: errors.length === 0 | valid: | ||
errors.length === 0 | |||
}; | }; | ||
}, | }, | ||
showValidationErrors(validation) { | showValidationErrors( | ||
validation | |||
) { | |||
this.renderAnalysisBySelector(); | this.renderAnalysisBySelector(); | ||
| Line 2,753: | Line 3,781: | ||
mw.notify( | mw.notify( | ||
validation.errors.join('\n'), | validation.errors.join('\n'), | ||
{ type: 'error' } | { | ||
type: 'error' | |||
} | |||
); | ); | ||
} | } | ||
}; | }; | ||
mw.PreloadEditor = PreloadEditor; | mw.PreloadEditor = | ||
PreloadEditor; | |||
$(function () { | $(function () { | ||