diff --git a/i18n/cte/en.json b/i18n/cte/en.json index 215eed3f..898a3d3b 100644 --- a/i18n/cte/en.json +++ b/i18n/cte/en.json @@ -5,8 +5,8 @@ "deputy.cte.close": "Close", "deputy.cte.save": "Save", "deputy.cte.add": "Add a notice", - "deputy.cte.merge": "Merge all notices", - "deputy.cte.merge.confirm": "You are about to merge $1 {{PLURAL:$1|notice|notices}}. Continue?", + "deputy.cte.mergeAll": "Merge all notices", + "deputy.cte.mergeAll.confirm": "You are about to merge $1 {{PLURAL:$1|notice|notices}}. Continue?", "deputy.cte.reset": "Reset all changes", "deputy.cte.reset.confirm": "This will reset all changes. Proceed?", "deputy.cte.delete": "Delete all notices", @@ -23,15 +23,16 @@ "deputy.cte.empty.add": "Add a notice", "deputy.cte.noSpot": "Sorry, but a notice cannot be automatically added. Please contact the developer to possibly add support for this talk page.", + "deputy.cte.merge": "Merge", + "deputy.cte.merge.title": "Merge notices", + "deputy.cte.merge.from.label": "Notices to merge", + "deputy.cte.merge.from.select": "Select a notice", + "deputy.cte.merge.from.empty": "No notices to merge", + "deputy.cte.merge.all": "Merge all", + "deputy.cte.merge.all.confirm": "You are about to merge $1 'copied' {{PLURAL:$1|notice|notices}} into this notice. Continue?", + "deputy.cte.merge.button": "Merge", + "deputy.cte.copied.label": "Copied $1", - "deputy.cte.copied.merge": "Merge", - "deputy.cte.copied.merge.title": "Merge 'copied' templates", - "deputy.cte.copied.merge.from.label": "Notices to merge", - "deputy.cte.copied.merge.from.select": "Select a notice", - "deputy.cte.copied.merge.from.empty": "No notices to merge", - "deputy.cte.copied.merge.all": "Merge all", - "deputy.cte.copied.merge.all.confirm": "You are about to merge $1 'copied' {{PLURAL:$1|notice|notices}} into this notice. Continue?", - "deputy.cte.copied.merge.button": "Merge", "deputy.cte.copied.remove": "Remove notice", "deputy.cte.copied.remove.confirm": "This will destroy $1 {{PLURAL:$1|entry|entries}}. Continue?", @@ -44,6 +45,9 @@ "deputy.cte.copied.entry.copy.lacking": "Attribution edit summary copied to clipboard with lacking properties. Ensure that `from` is supplied.", "deputy.cte.copied.entry.copy.success": "Attribution edit summary copied to clipboard.", + "deputy.cte.copied.collapse": "Collapse", + "deputy.cte.copied.small": "Small", + "deputy.cte.copied.from.placeholder": "Page A", "deputy.cte.copied.from.label": "Page copied from", "deputy.cte.copied.from.help": "This is the page from which the content was copied from.", @@ -74,5 +78,28 @@ "deputy.cte.copied.diffDeprecate.replace": "The current value of '$1', \"$2\", will be replaced with \"$3\". Continue?", "deputy.cte.copied.diffDeprecate.failed": "Cannot convert `diff` parameter to URL. See your browser console for more details.", - "deputy.cte.splitArticle.label": "Split article $1" + "deputy.cte.splitArticle.label": "Split article $1", + + "deputy.cte.splitArticle.remove": "Remove notice", + "deputy.cte.splitArticle.remove.confirm": "This will destroy $1 {{PLURAL:$1|entry|entries}}. Continue?", + "deputy.cte.splitArticle.add": "Add entry", + + "deputy.cte.splitArticle.entry.label": "Template entry", + "deputy.cte.splitArticle.entry.remove": "Remove entry", + + "deputy.cte.splitArticle.collapse": "Collapse", + "deputy.cte.splitArticle.from": "From", + "deputy.cte.splitArticle.from.help": "This is the page where the content was split from. In most cases, this is the current page, and can be left blank.", + + "deputy.cte.splitArticle.to.placeholder": "Subpage A", + "deputy.cte.splitArticle.to.label": "Page split to", + "deputy.cte.splitArticle.to.help": "This is the name of page that material was copied to; the \"merge target\".", + "deputy.cte.splitArticle.from_oldid.placeholder": "from_oldid", + "deputy.cte.splitArticle.from_oldid.label": "As of revision ID", + "deputy.cte.splitArticle.from_oldid.help": "The revision ID of the original page prior to the split. This is the revision that still contains content that will eventually become part of the split, with the following revision (or succeeding revisions) progressively transferring content to the other pages.", + "deputy.cte.splitArticle.date.label": "Date of split", + "deputy.cte.splitArticle.date.help": "The date that the split occurred.", + "deputy.cte.splitArticle.diff.placeholder": "https://en.wikipedia.org/w/index.php?diff=123456&oldid=123455", + "deputy.cte.splitArticle.diff.label": "Diff URL", + "deputy.cte.splitArticle.diff.help": "The diff URL of the split." } diff --git a/src/modules/cte/CopiedTemplateEditor.ts b/src/modules/cte/CopiedTemplateEditor.ts index 9a78d9c6..55742749 100644 --- a/src/modules/cte/CopiedTemplateEditor.ts +++ b/src/modules/cte/CopiedTemplateEditor.ts @@ -125,7 +125,11 @@ export default class CopiedTemplateEditor { mw.hook( 'wikipage.content' ).add( () => { // Find all {{copied}} templates and append our special button. // This runs on the actual document, not the Parsoid document. - document.querySelectorAll( '.copiednotice > tbody > tr' ) + document.querySelectorAll( + [ 'copiednotice', 'box-split-article' ].map( + ( v ) => `.${v} > tbody > tr` + ).join( ', ' ) + ) .forEach( ( e ) => { if ( e.classList.contains( 'cte-upgraded' ) ) { return; @@ -175,6 +179,9 @@ export default class CopiedTemplateEditor { 'mediawiki.widgets.datetime', 'jquery.makeCollapsible' ], async () => { + OO.ui.WindowManager.static.sizes.huge = { + width: 1100 + }; mw.util.addCSS( cteStyles ); await WikiAttributionNotices.init(); diff --git a/src/modules/cte/css/copied-template-editor.css b/src/modules/cte/css/copied-template-editor.css index 9d353158..c37d239f 100644 --- a/src/modules/cte/css/copied-template-editor.css +++ b/src/modules/cte/css/copied-template-editor.css @@ -1,3 +1,16 @@ +.copied-template-editor .oo-ui-window-frame { + width: 1000px !important; +} + +.copied-template-editor .oo-ui-menuLayout > .oo-ui-menuLayout-menu { + height: 20em; + width: 20em; +} + +.copied-template-editor .oo-ui-menuLayout > .oo-ui-menuLayout-content { + left: 20em; +} + .cte-preview .copiednotice { margin-left: 0; margin-right: 0; @@ -31,11 +44,11 @@ .cte-templateOptions { margin: 8px; + display: flex; } -.cte-templateOptions > div { - width: 50%; - display: inline-block; +.cte-templateOptions > * { + flex: 1; } .cte-fieldset { @@ -64,7 +77,7 @@ margin-right: 16px; } -.cte-fieldset-date .oo-ui-iconElement-icon { +.copied-template-editor .mw-widgets-datetime-dateTimeInputWidget-handle .oo-ui-iconElement-icon { left: 0.5em; width: 1em; height: 1em; diff --git a/src/modules/cte/models/AttributionNotice.ts b/src/modules/cte/models/AttributionNotice.ts index b0c0fc52..4b768bac 100644 --- a/src/modules/cte/models/AttributionNotice.ts +++ b/src/modules/cte/models/AttributionNotice.ts @@ -1,7 +1,7 @@ import CTEParsoidDocument from './CTEParsoidDocument'; import { MediaWikiData, TemplateData, TemplateDataModifier } from './MediaWikiData'; import { AttributionNoticePageLayout } from '../ui/pages/AttributionNoticePageLayout'; -import { AttributionNoticePageGenerator } from '../ui/pages/AttributionNoticePageGenerator'; +import { AttributionNoticePageGenerator } from '../ui/AttributionNoticePageGenerator'; import { CTEParsoidTransclusionTemplateNode } from './CTEParsoidTransclusionTemplateNode'; /** diff --git a/src/modules/cte/models/AttributionNoticeRow.ts b/src/modules/cte/models/AttributionNoticeRow.ts index 0374a104..614959bd 100644 --- a/src/modules/cte/models/AttributionNoticeRow.ts +++ b/src/modules/cte/models/AttributionNoticeRow.ts @@ -1,17 +1,48 @@ -import type AttributionNotice from './AttributionNotice'; -import { AttributionNoticePageGenerator } from '../ui/pages/AttributionNoticePageGenerator'; +import { AttributionNoticePageGenerator } from '../ui/AttributionNoticePageGenerator'; import { AttributionNoticePageLayout } from '../ui/pages/AttributionNoticePageLayout'; +export interface AttributionNoticeRowParent { + addRow( row: any ): void; + deleteRow( row: any ): void; +} + /** * An attribution notice's row or entry. */ -export abstract class AttributionNoticeRow +export abstract class AttributionNoticeRow implements AttributionNoticePageGenerator { + protected _parent: T; /** * @return The parent of this attribution notice row. */ - abstract get parent(): T; + get parent(): T { + return this._parent; + } + + /** + * Sets the parent. Automatically moves this template from one + * parent's row set to another. + * + * @param newParent The new parent. + */ + set parent( newParent ) { + this._parent.deleteRow( this ); + newParent.addRow( this ); + this._parent = newParent; + } + abstract generatePage( dialog: any ): AttributionNoticePageLayout; + /** + * Clones this row. + * + * @param parent The parent of this new row. + * @return The cloned row + */ + clone( parent: T ): AttributionNoticeRow { + // noinspection JSCheckFunctionSignatures + return new ( this.constructor as any )( this, parent ); + } + } diff --git a/src/modules/cte/models/CTEParsoidDocument.ts b/src/modules/cte/models/CTEParsoidDocument.ts index c83437b9..0bdbd2e0 100644 --- a/src/modules/cte/models/CTEParsoidDocument.ts +++ b/src/modules/cte/models/CTEParsoidDocument.ts @@ -1,8 +1,10 @@ import ParsoidDocument from '@chlodalejandro/parsoid'; import last from '../../../util/last'; import AttributionNotice from './AttributionNotice'; -import WikiAttributionNotices, { SupportedAttributionNoticeType } from './WikiAttributionNotices'; -import CopiedTemplate from './templates/CopiedTemplate'; +import WikiAttributionNotices, { + AttributionNoticeTypeClass, + SupportedAttributionNoticeType +} from './WikiAttributionNotices'; import TemplateInsertEvent from '../events/TemplateInsertEvent'; import { CTEParsoidTransclusionTemplateNode } from './CTEParsoidTransclusionTemplateNode'; import TemplateFactory from './TemplateFactory'; @@ -52,7 +54,7 @@ export default class CTEParsoidDocument extends ParsoidDocument { return; } - const notices = this.findCopiedNotices(); + const notices = this.findNoticeType( 'copied' ); this.originalCount = notices.length; } ); } @@ -102,13 +104,16 @@ export default class CTEParsoidDocument extends ParsoidDocument { /** * Finds this document's {{copied}} notices. * + * @param type * @return An array of all CopiedTemplate objects found */ - findCopiedNotices(): CopiedTemplate[] { + findNoticeType( + type: T + ): AttributionNoticeTypeClass[] { return this.findNotices().filter( ( notice ) => notice instanceof - WikiAttributionNotices.attributionNoticeClasses.copied - ) as CopiedTemplate[]; + WikiAttributionNotices.attributionNoticeClasses[ type ] + ) as AttributionNoticeTypeClass[]; } /** diff --git a/src/modules/cte/models/RowedAttributionNotice.ts b/src/modules/cte/models/RowedAttributionNotice.ts index 1156ca03..44ebc980 100644 --- a/src/modules/cte/models/RowedAttributionNotice.ts +++ b/src/modules/cte/models/RowedAttributionNotice.ts @@ -1,12 +1,14 @@ import AttributionNotice from './AttributionNotice'; import RowChangeEvent from '../events/RowChangeEvent'; +import { AttributionNoticeRow, AttributionNoticeRowParent } from './AttributionNoticeRow'; /** * This is a sub-abstract class of {@link AttributionNotice} that represents any * attribution notice template that can contain multiple entries (or rows). */ -export default abstract class RowedAttributionNotice - extends AttributionNotice { +export default abstract class RowedAttributionNotice< + RowClass extends AttributionNoticeRow> + > extends AttributionNotice { /** * All the rows of this template. @@ -96,6 +98,41 @@ export default abstract class RowedAttributionNotice this.save(); this.dispatchEvent( new RowChangeEvent( 'rowDelete', row ) ); } + + if ( this._rows.length === 0 ) { + this.destroy(); + } + } + + /** + * Copies in the rows of another {@link SplitArticleTemplate}, and + * optionally deletes that template or clears its contents. + * + * @param template The template to copy from. + * @param options Options for this merge. + * @param options.delete + * Whether the reference template will be deleted after merging. + * @param options.clear + * Whether the reference template's rows will be cleared after merging. + */ + merge( + template: RowedAttributionNotice, + options: { delete?: boolean, clear?: boolean } = {} + ) { + if ( template.rows === undefined || template === this ) { + // Deleted or self + return; + } + for ( const row of template.rows ) { + if ( options.clear ) { + row.parent = this; + } else { + ( this as AttributionNoticeRowParent ).addRow( row.clone( this ) ); + } + } + if ( options.delete ) { + template.destroy(); + } } } diff --git a/src/modules/cte/models/TemplateFactory.ts b/src/modules/cte/models/TemplateFactory.ts index bb32d294..b6f13f1a 100644 --- a/src/modules/cte/models/TemplateFactory.ts +++ b/src/modules/cte/models/TemplateFactory.ts @@ -38,7 +38,7 @@ export default class TemplateFactory { * @return A new CopiedTemplate */ static copied( document: CTEParsoidDocument ): CopiedTemplate { - const templateWikitext = this.getTemplateWikitext( 'copied' ); + const templateWikitext = TemplateFactory.getTemplateWikitext( 'copied' ); const node = CTEParsoidTransclusionTemplateNode.fromNew( document, templateWikitext, @@ -59,12 +59,14 @@ export default class TemplateFactory { * @return A new SplitArticleTemplate */ static splitArticle( document: CTEParsoidDocument ): SplitArticleTemplate { - const templateWikitext = this.getTemplateWikitext( 'splitArticle' ); + const templateWikitext = TemplateFactory.getTemplateWikitext( 'splitArticle' ); const node = CTEParsoidTransclusionTemplateNode.fromNew( document, templateWikitext, { - from: new mw.Title( document.getPage() ).getSubjectPage().getPrefixedText() + from: new mw.Title( document.getPage() ).getSubjectPage().getPrefixedText(), + // Blank string to trigger row creation + to: '' } ); node.element.setAttribute( 'about', `N${TemplateFactory.noticeCount++}` ); diff --git a/src/modules/cte/models/WikiAttributionNotices.ts b/src/modules/cte/models/WikiAttributionNotices.ts index 6eca7171..c65d0877 100644 --- a/src/modules/cte/models/WikiAttributionNotices.ts +++ b/src/modules/cte/models/WikiAttributionNotices.ts @@ -167,3 +167,6 @@ export default class WikiAttributionNotices { } } + +export type AttributionNoticeTypeClass = + InstanceType; diff --git a/src/modules/cte/models/templates/CopiedTemplate.ts b/src/modules/cte/models/templates/CopiedTemplate.ts index 9b106d6c..3ff54402 100644 --- a/src/modules/cte/models/templates/CopiedTemplate.ts +++ b/src/modules/cte/models/templates/CopiedTemplate.ts @@ -4,7 +4,7 @@ import CopiedTemplateRow, { } from './CopiedTemplateRow'; import { AttributionNoticePageLayout } from '../../ui/pages/AttributionNoticePageLayout'; import CopiedTemplatePage from '../../ui/pages/CopiedTemplatePage'; -import { AttributionNoticePageGenerator } from '../../ui/pages/AttributionNoticePageGenerator'; +import { AttributionNoticePageGenerator } from '../../ui/AttributionNoticePageGenerator'; import RowedAttributionNotice from '../RowedAttributionNotice'; /** @@ -129,10 +129,10 @@ export default class CopiedTemplate * Destroys this template completely. */ destroy() { - this.dispatchEvent( new Event( 'destroy' ) ); - this.accessTemplateData( () => undefined ); + this.node.destroy(); // Self-destruct Object.keys( this ).forEach( ( k ) => delete ( this as any )[ k ] ); + this.dispatchEvent( new Event( 'destroy' ) ); } /** diff --git a/src/modules/cte/models/templates/CopiedTemplateRow.ts b/src/modules/cte/models/templates/CopiedTemplateRow.ts index 0d81b515..c41e44ee 100644 --- a/src/modules/cte/models/templates/CopiedTemplateRow.ts +++ b/src/modules/cte/models/templates/CopiedTemplateRow.ts @@ -64,16 +64,6 @@ export interface RawCopiedTemplateRow { merge?: string; } -/** - * Special interface that requires the "from" parameter. - */ -export interface ExistingRawCopiedTemplateRow extends RawCopiedTemplateRow { - /** - * @inheritDoc - */ - from: string; -} - /** * Represents a row/entry in a {{copied}} template. */ @@ -104,32 +94,7 @@ export default class CopiedTemplateRow id: string; - /** - * The parent of a given copied template row. This is the {{copied}} template - * that this row is a part of. - */ - private _parent: CopiedTemplate; - - /** - * @return The parent of a given copied template row. This is the {{copied}} - * template that this row is a part of. - */ - get parent() { - return this._parent; - } - - /** - * Sets the parent. Automatically moves this template from one - * parent's row set to another. - * - * @param newParent The new parent. - */ - set parent( newParent ) { - this._parent.deleteRow( this ); - newParent.addRow( this ); - this._parent = newParent; - } - + // noinspection JSDeprecatedSymbols /** * Creates a new RawCopiedTemplateRow * @@ -163,14 +128,10 @@ export default class CopiedTemplateRow } /** - * Clones this row. - * - * @param parent The parent of this new row. - * @return The cloned row + * @inheritDoc */ clone( parent: CopiedTemplate ): CopiedTemplateRow { - // noinspection JSCheckFunctionSignatures - return new CopiedTemplateRow( this, parent ); + return super.clone( parent ) as CopiedTemplateRow; } /** diff --git a/src/modules/cte/models/templates/SplitArticleTemplate.ts b/src/modules/cte/models/templates/SplitArticleTemplate.ts index 501e6c9f..b7588e00 100644 --- a/src/modules/cte/models/templates/SplitArticleTemplate.ts +++ b/src/modules/cte/models/templates/SplitArticleTemplate.ts @@ -1,4 +1,4 @@ -import { AttributionNoticePageGenerator } from '../../ui/pages/AttributionNoticePageGenerator'; +import { AttributionNoticePageGenerator } from '../../ui/AttributionNoticePageGenerator'; import { AttributionNoticePageLayout } from '../../ui/pages/AttributionNoticePageLayout'; import { copiedTemplateRowParameters } from './CopiedTemplateRow'; import SplitArticleTemplateRow, { @@ -16,14 +16,18 @@ export default class SplitArticleTemplate implements AttributionNoticePageGenerator { from: string; + collapse: boolean; /** * @inheritDoc */ parse(): void { - if ( this.node.getParameter( 'from' ) ) { + if ( this.node.hasParameter( 'from' ) ) { this.from = this.node.getParameter( 'from' ).trim(); } + if ( this.node.hasParameter( 'collapse' ) ) { + this.collapse = this.node.getParameter( 'collapse' ).trim().length > 0; + } // Extract {{copied}} rows. const rows = []; @@ -64,8 +68,6 @@ export default class SplitArticleTemplate * @inheritDoc */ save(): void { - this.node.setParameter( 'from', this.from ); - const existingParameters = this.node.getParameters(); for ( const param in existingParameters ) { if ( copiedTemplateRowParameters.some( ( v ) => param.startsWith( v ) ) ) { @@ -74,11 +76,14 @@ export default class SplitArticleTemplate } } + this.node.setParameter( 'collapse', this.collapse ? 'yes' : null ); + this.node.setParameter( 'from', this.from ); + this._rows.forEach( ( row, i ) => { - this.node.setParameter( `to${i > 1 ? i : ''}`, row.to ); - this.node.setParameter( `from_oldid${i > 1 ? i : ''}`, row.from_oldid ); - this.node.setParameter( `date${i > 1 ? i : ''}`, row.date ); - this.node.setParameter( `diff${i > 1 ? i : ''}`, row.diff ); + this.node.setParameter( `to${i > 0 ? i + 1 : ''}`, row.to ); + this.node.setParameter( `from_oldid${i > 0 ? i + 1 : ''}`, row.from_oldid ); + this.node.setParameter( `date${i > 0 ? i + 1 : ''}`, row.date ); + this.node.setParameter( `diff${i > 0 ? i + 1 : ''}`, row.diff ); } ); this.dispatchEvent( new Event( 'save' ) ); diff --git a/src/modules/cte/models/templates/SplitArticleTemplateRow.ts b/src/modules/cte/models/templates/SplitArticleTemplateRow.ts index 51b4a53b..366293f1 100644 --- a/src/modules/cte/models/templates/SplitArticleTemplateRow.ts +++ b/src/modules/cte/models/templates/SplitArticleTemplateRow.ts @@ -1,18 +1,21 @@ +// noinspection JSDeprecatedSymbols + import SplitArticleTemplate from './SplitArticleTemplate'; import CopiedTemplateRowPage from '../../ui/pages/CopiedTemplateRowPage'; import { AttributionNoticeRow } from '../AttributionNoticeRow'; import SplitArticleTemplateRowPage from '../../ui/pages/SplitArticleTemplateRowPage'; export interface RawSplitArticleTemplateRow { - to: string; - from_oldid: string; - date: string; + to?: string; + from_oldid?: string; + date?: string; diff?: string; } export const splitArticleTemplateRowParameters = [ 'to', 'from_oldid', 'date', 'diff' ]; +export type SplitArticleTemplateRowParameter = typeof splitArticleTemplateRowParameters[number]; /** * Represents a row/entry in a {{split article}} template. @@ -33,32 +36,6 @@ export default class SplitArticleTemplateRow id: string; - /** - * The parent of a given copied template row. This is the {{split article}} template - * that this row is a part of. - */ - private _parent: SplitArticleTemplate; - - /** - * @return The parent of a given copied template row. This is the {{split article}} - * template that this row is a part of. - */ - get parent() { - return this._parent; - } - - /** - * Sets the parent. Automatically moves this template from one - * parent's row set to another. - * - * @param newParent The new parent. - */ - set parent( newParent ) { - this._parent.deleteRow( this ); - newParent.addRow( this ); - this._parent = newParent; - } - /** * Creates a new RawCopiedTemplateRow * @@ -86,14 +63,10 @@ export default class SplitArticleTemplateRow } /** - * Clones this row. - * - * @param parent The parent of this new row. - * @return The cloned row + * @inheritDoc */ clone( parent: SplitArticleTemplate ): SplitArticleTemplateRow { - // noinspection JSCheckFunctionSignatures - return new SplitArticleTemplateRow( this, parent ); + return super.clone( parent ) as SplitArticleTemplateRow; } /** diff --git a/src/modules/cte/ui/pages/AttributionNoticePageGenerator.ts b/src/modules/cte/ui/AttributionNoticePageGenerator.ts similarity index 79% rename from src/modules/cte/ui/pages/AttributionNoticePageGenerator.ts rename to src/modules/cte/ui/AttributionNoticePageGenerator.ts index 369c104c..07dd35f0 100644 --- a/src/modules/cte/ui/pages/AttributionNoticePageGenerator.ts +++ b/src/modules/cte/ui/AttributionNoticePageGenerator.ts @@ -1,4 +1,4 @@ -import { AttributionNoticePageLayout } from './AttributionNoticePageLayout'; +import { AttributionNoticePageLayout } from './pages/AttributionNoticePageLayout'; export interface AttributionNoticePageGenerator { diff --git a/src/modules/cte/ui/CopiedTemplateEditorDialog.tsx b/src/modules/cte/ui/CopiedTemplateEditorDialog.tsx index c3f77757..19ead234 100644 --- a/src/modules/cte/ui/CopiedTemplateEditorDialog.tsx +++ b/src/modules/cte/ui/CopiedTemplateEditorDialog.tsx @@ -35,7 +35,7 @@ function initCopiedTemplateEditorDialog() { static static = { name: 'copiedTemplateEditorDialog', title: mw.message( 'deputy.cte' ).text(), - size: 'larger', + size: 'huge', actions: [ { flags: [ 'primary', 'progressive' ], @@ -92,7 +92,7 @@ function initCopiedTemplateEditorDialog() { * @return The body height of this dialog. */ getBodyHeight(): number { - return 500; + return 900; } /** @@ -185,16 +185,17 @@ function initCopiedTemplateEditorDialog() { icon: 'tableMergeCells', framed: false, invisibleLabel: true, - label: mw.message( 'deputy.cte.merge' ).text(), - title: mw.message( 'deputy.cte.merge' ).text(), + label: mw.message( 'deputy.cte.mergeAll' ).text(), + title: mw.message( 'deputy.cte.mergeAll' ).text(), disabled: true } ); + // TODO: Repair mergeButton this.mergeButton.on( 'click', () => { - const notices = this.parsoid.findCopiedNotices(); + const notices = this.parsoid.findNoticeType( 'copied' ); if ( notices.length > 1 ) { return OO.ui.confirm( mw.message( - 'deputy.cte.merge.confirm', + 'deputy.cte.mergeAll.confirm', `${notices.length}` ).text() ).done( ( confirmed: boolean ) => { @@ -256,12 +257,14 @@ function initCopiedTemplateEditorDialog() { this.layout.on( 'remove', () => { const notices = this.parsoid.findNotices(); - this.mergeButton.setDisabled( notices.length < 2 ); + // TODO: Repair mergeButton + // this.mergeButton.setDisabled( notices.length < 2 ); deleteButton.setDisabled( notices.length === 0 ); } ); this.parsoid.addEventListener( 'templateInsert', () => { const notices = this.parsoid.findNotices(); - this.mergeButton.setDisabled( notices.length < 2 ); + // TODO: Repair mergeButton + // this.mergeButton.setDisabled( notices.length < 2 ); deleteButton.setDisabled( notices.length === 0 ); } ); @@ -328,7 +331,7 @@ function initCopiedTemplateEditorDialog() { // Recheck state of merge button this.mergeButton.setDisabled( - ( this.parsoid.findCopiedNotices().length ?? 0 ) < 2 + ( this.parsoid.findNoticeType( 'copied' ).length ?? 0 ) < 2 ); process.next( () => { diff --git a/src/modules/cte/ui/RowPageShared.tsx b/src/modules/cte/ui/RowPageShared.tsx new file mode 100644 index 00000000..a8826f93 --- /dev/null +++ b/src/modules/cte/ui/RowPageShared.tsx @@ -0,0 +1,180 @@ +import unwrapWidget from '../../../util/unwrapWidget'; +import TemplateMerger from '../models/TemplateMerger'; +import { + AttributionNoticeTypeClass, + SupportedAttributionNoticeType +} from '../models/WikiAttributionNotices'; +import removeElement from '../../../util/removeElement'; +import { h } from 'tsx-dom'; +import AttributionNotice from '../models/AttributionNotice'; + +/** + * Renders the panel used to merge multiple {{split article}} templates. + * + * @param type + * @param parentTemplate + * @param mergeButton + * @return A
element + */ +export function renderMergePanel( + type: T, + parentTemplate: AttributionNoticeTypeClass, + mergeButton: any +): JSX.Element { + const mergePanel = new OO.ui.FieldsetLayout( { + classes: [ 'cte-merge-panel' ], + icon: 'tableMergeCells', + label: mw.message( 'deputy.cte.merge.title' ).text() + } ); + unwrapWidget( mergePanel ).style.padding = '16px'; + unwrapWidget( mergePanel ).style.zIndex = '20'; + // Hide by default + mergePanel.toggle( false ); + + // and button for merging templates - const mergeTarget = new OO.ui.DropdownInputWidget( { - $overlay: true, - label: mw.message( 'deputy.cte.copied.merge.from.select' ).text() - } ); - const mergeTargetButton = new OO.ui.ButtonWidget( { - label: mw.message( 'deputy.cte.copied.merge.button' ).text() - } ); - mergeTargetButton.on( 'click', () => { - const template = this.document.findCopiedNotices().find( - ( v ) => v.name === mergeTarget.value - ); - if ( template ) { - // If template found, merge and reset panel - this.copiedTemplate.merge( template, { delete: true } ); - mergeTarget.setValue( null ); - mergePanel.toggle( false ); - } - } ); - - const mergeFieldLayout = new OO.ui.ActionFieldLayout( - mergeTarget, - mergeTargetButton, - { - label: mw.message( 'deputy.cte.copied.merge.from.label' ).text(), - align: 'left' - } + return renderMergePanel( + 'copied', this.copiedTemplate, this.mergeButton ); - this.mergeButton.on( 'click', () => { - mergePanel.toggle(); - } ); - const mergeAllButton = new OO.ui.ButtonWidget( { - label: mw.message( 'deputy.cte.copied.merge.all' ).text(), - flags: [ 'progressive' ] - } ); - mergeAllButton.on( 'click', () => { - const notices = this.document.findCopiedNotices(); - // Confirm before merging. - OO.ui.confirm( - mw.message( - 'deputy.cte.copied.merge.all.confirm', - `${notices.length - 1}` - ).text() - ).done( ( confirmed: boolean ) => { - if ( confirmed ) { - // Recursively merge all templates - TemplateMerger.copied( - notices, - this.copiedTemplate - ); - mergeTarget.setValue( null ); - mergePanel.toggle( false ); - } - } ); - } ); - - const recalculateOptions = () => { - const notices = this.document.findCopiedNotices(); - const options = []; - for ( const notice of notices ) { - if ( notice === this.copiedTemplate ) { - continue; - } - options.push( { - data: notice.name, - label: `Copied ${notice.name}` - } ); - } - if ( options.length === 0 ) { - options.push( { - data: null, - label: mw.message( 'deputy.cte.copied.merge.from.empty' ).text(), - disabled: true - } ); - mergeTargetButton.setDisabled( true ); - mergeAllButton.setDisabled( true ); - } else { - mergeTargetButton.setDisabled( false ); - mergeAllButton.setDisabled( false ); - } - mergeTarget.setOptions( options ); - }; - mergePanel.on( 'toggle', recalculateOptions ); - - mergePanel.addItems( [ mergeFieldLayout, mergeAllButton ] ); - return unwrapWidget( mergePanel ); - } - - /** - * Updates the preview panel rendered with `renderPreviewPanel()`. - */ - private async updatePreviewPanel(): Promise { - if ( !this.previewPanel ) { - // Skip if still unavailable. - return; - } - - await this.copiedTemplate.generatePreview().then( ( data ) => { - this.previewPanel.innerHTML = data; - - // Remove DiscussionTools empty talk page notice - const emptyStateNotice = this.previewPanel.querySelector( - '.ext-discussiontools-emptystate' - ); - if ( emptyStateNotice ) { - removeElement( emptyStateNotice ); - } - - // Make all anchor links open in a new tab (prevents exit navigation) - this.previewPanel.querySelectorAll( 'a' ) - .forEach( ( el: HTMLElement ) => { - el.setAttribute( 'target', '_blank' ); - el.setAttribute( 'rel', 'noopener' ); - } ); - - // Infuse collapsibles - ( $( this.previewPanel ).find( '.collapsible' ) as any ) - .makeCollapsible(); - } ); - } - - /** - * Renders the preview "panel". Not an actual panel, but rather a
that - * shows a preview of the template to be saved. - * - * @return A
element, containing an HTML render of the template wikitext. - */ - renderPreviewPanel(): JSX.Element { - this.previewPanel =
as HTMLElement; - - // Listen for changes - this.copiedTemplate.addEventListener( 'save', () => { - this.updatePreview(); - } ); - this.updatePreview(); - - return this.previewPanel; } /** @@ -387,11 +242,11 @@ function initCopiedTemplatePage() { }; this.fields = { collapse: new OO.ui.FieldLayout( this.inputSet.collapse, { - label: 'Collapse', + label: mw.message( 'deputy.cte.copied.collapse' ).text(), align: 'inline' } ), small: new OO.ui.FieldLayout( this.inputSet.small, { - label: 'Small', + label: mw.message( 'deputy.cte.copied.small' ).text(), align: 'inline' } ) }; diff --git a/src/modules/cte/ui/pages/CopiedTemplateRowPage.tsx b/src/modules/cte/ui/pages/CopiedTemplateRowPage.tsx index f5bd9fca..ddac1e5e 100644 --- a/src/modules/cte/ui/pages/CopiedTemplateRowPage.tsx +++ b/src/modules/cte/ui/pages/CopiedTemplateRowPage.tsx @@ -332,9 +332,9 @@ function initCopiedTemplateRowPage() { } ), merge: new OO.ui.FieldLayout( this.inputs.merge, { $overlay: this.parent.$overlay, - label: mw.message( 'deputy.cte.copied.merge.label' ).text(), + label: mw.message( 'deputy.cte.merge.label' ).text(), align: 'inline', - help: mw.message( 'deputy.cte.copied.merge.help' ).text() + help: mw.message( 'deputy.cte.merge.help' ).text() } ), afd: new OO.ui.FieldLayout( this.inputs.afd, { $overlay: this.parent.$overlay, diff --git a/src/modules/cte/ui/pages/SplitArticleTemplatePage.tsx b/src/modules/cte/ui/pages/SplitArticleTemplatePage.tsx index 54cbd221..a08e2b8d 100644 --- a/src/modules/cte/ui/pages/SplitArticleTemplatePage.tsx +++ b/src/modules/cte/ui/pages/SplitArticleTemplatePage.tsx @@ -4,6 +4,9 @@ import SplitArticleTemplate from '../../models/templates/SplitArticleTemplate'; import SplitArticleTemplateRowPage from './splitArticleTemplateRowPage'; import { AttributionNoticePageLayout } from './AttributionNoticePageLayout'; import { h } from 'tsx-dom'; +import unwrapWidget from '../../../../util/unwrapWidget'; +import CTEParsoidDocument from '../../models/CTEParsoidDocument'; +import { renderMergePanel, renderPreviewPanel } from '../RowPageShared'; export interface SplitArticleTemplatePageData { /** @@ -36,6 +39,15 @@ function initSplitArticleTemplatePage() { * @inheritDoc */ parent: /* splitArticleTemplateEditorDialog */ any; + /** + * The CTEParsoidDocument that this page refers to. + */ + document: CTEParsoidDocument; + + /** + * Label for this page. + */ + label: string; /** * All child pages of this splitArticleTemplatePage. Garbage collected when rechecked. @@ -88,7 +100,15 @@ function initSplitArticleTemplatePage() { } ); this.$element.append( - this.renderHeader() + this.renderButtons(), + this.renderHeader(), + renderMergePanel( + 'splitArticle', + this.splitArticleTemplate, + this.mergeButton + ), + renderPreviewPanel( this.splitArticleTemplate ), + this.renderTemplateOptions() ); } @@ -109,13 +129,71 @@ function initSplitArticleTemplatePage() { // Delete deleted rows from cache. this.childPages.forEach( ( page, row ) => { if ( rowPages.indexOf( page ) === -1 ) { - this.pageCache.delete( row ); + this.childPages.delete( row ); } } ); return rowPages; } + /** + * Renders the set of buttons that appear at the top of the page. + * + * @return A
element. + */ + renderButtons(): JSX.Element { + const buttonSet =
; + + this.mergeButton = new OO.ui.ButtonWidget( { + icon: 'tableMergeCells', + title: mw.message( 'deputy.cte.merge' ).text(), + framed: false + } ); + const deleteButton = new OO.ui.ButtonWidget( { + icon: 'trash', + title: mw.message( 'deputy.cte.splitArticle.remove' ).text(), + framed: false, + flags: [ 'destructive' ] + } ); + deleteButton.on( 'click', () => { + if ( this.splitArticleTemplate.rows.length > 0 ) { + OO.ui.confirm( + mw.message( + 'deputy.cte.splitArticle.remove.confirm', + `${this.splitArticleTemplate.rows.length}` + ).text() + ).done( ( confirmed: boolean ) => { + if ( confirmed ) { + this.splitArticleTemplate.destroy(); + } + } ); + } else { + this.splitArticleTemplate.destroy(); + } + } ); + const addButton = new OO.ui.ButtonWidget( { + flags: [ 'progressive' ], + icon: 'add', + label: mw.message( 'deputy.cte.splitArticle.add' ).text() + } ); + addButton.on( 'click', () => { + this.splitArticleTemplate.addRow( new SplitArticleTemplateRow( + {}, this.splitArticleTemplate + ) ); + } ); + + this.splitArticleTemplate.addEventListener( 'rowAdd', () => { + // TODO: Remove after template improvements. + addButton.setDisabled( this.splitArticleTemplate.rows.length >= 10 ); + } ); + + buttonSet.appendChild( unwrapWidget( this.mergeButton ) ); + buttonSet.appendChild( unwrapWidget( deleteButton ) ); + buttonSet.appendChild( unwrapWidget( addButton ) ); + + return buttonSet; + } + /** * @return The rendered header of this PageLayout. */ @@ -123,6 +201,49 @@ function initSplitArticleTemplatePage() { return

{ this.label }

; } + /** + * Renders the global options of this template. This includes parameters that are not + * counted towards an entry and affect the template as a whole. + * + * @return A
element. + */ + renderTemplateOptions(): JSX.Element { + const page = new mw.Title( + this.splitArticleTemplate.parsoid.getPage() + ).getSubjectPage().getPrefixedText(); + + const collapse = new OO.ui.CheckboxInputWidget( { + value: this.splitArticleTemplate.collapse + } ); + const from = new mw.widgets.TitleInputWidget( { + $overlay: this.parent.$overlay, + value: this.splitArticleTemplate.from || '', + placeholder: page + } ); + + collapse.on( 'change', ( value: boolean ) => { + this.splitArticleTemplate.collapse = value; + this.splitArticleTemplate.save(); + } ); + from.on( 'change', ( value: string ) => { + this.splitArticleTemplate.from = value.length > 0 ? value : page; + this.splitArticleTemplate.save(); + } ); + + return
+
{ unwrapWidget( new OO.ui.FieldLayout( collapse, { + $overlay: this.parent.$overlay, + align: 'inline', + label: mw.message( 'deputy.cte.splitArticle.collapse' ).text() + } ) )}
+
{ unwrapWidget( new OO.ui.FieldLayout( from, { + $overlay: this.parent.$overlay, + label: mw.message( 'deputy.cte.splitArticle.from' ).text(), + help: mw.message( 'deputy.cte.splitArticle.from.help' ).text() + } ) ) }
+
; + } + /** * Sets up the outline item of this page. Used in the BookletLayout. */ diff --git a/src/modules/cte/ui/pages/SplitArticleTemplateRowPage.tsx b/src/modules/cte/ui/pages/SplitArticleTemplateRowPage.tsx index 91b59e3d..e0f45ce2 100644 --- a/src/modules/cte/ui/pages/SplitArticleTemplateRowPage.tsx +++ b/src/modules/cte/ui/pages/SplitArticleTemplateRowPage.tsx @@ -1,8 +1,12 @@ import '../../../../types'; import { AttributionNoticePageLayout } from './AttributionNoticePageLayout'; import CopiedTemplateEditorDialog from '../CopiedTemplateEditorDialog'; -import SplitArticleTemplateRow from '../../models/templates/SplitArticleTemplateRow'; +import SplitArticleTemplateRow, { + SplitArticleTemplateRowParameter +} from '../../models/templates/SplitArticleTemplateRow'; import { h } from 'tsx-dom'; +import getObjectValues from '../../../../util/getObjectValues'; +import unwrapWidget from '../../../../util/unwrapWidget'; export interface SplitArticleTemplateRowPageData { /** @@ -30,6 +34,10 @@ function initSplitArticleTemplateRowPage() { splitArticleTemplateRow: SplitArticleTemplateRow; parent: ReturnType; + // Elements + inputs: Record; + fieldLayouts: Record; + /** * @param config Configuration to be passed to the element. */ @@ -72,18 +80,169 @@ function initSplitArticleTemplateRowPage() { render() { this.layout = new OO.ui.FieldsetLayout( { icon: 'parameter', - label: mw.message( 'deputy.cte.copied.entry.label' ).text(), + label: mw.message( 'deputy.cte.splitArticle.entry.label' ).text(), classes: [ 'cte-fieldset' ] } ); - this.layout.$element.append(
- ${this.splitArticleTemplateRow.to} || ${this.splitArticleTemplateRow.from_oldid} || - ${this.splitArticleTemplateRow.date} || ${this.splitArticleTemplateRow.diff} -
); + this.layout.$element.append( this.renderButtons() ); + this.layout.addItems( this.renderFields() ); return this.layout; } + /** + * Renders a set of buttons used to modify a specific {{copied}} template row. + * + * @return An array of OOUI FieldLayouts + */ + renderButtons(): JSX.Element { + const deleteButton = new OO.ui.ButtonWidget( { + icon: 'trash', + title: mw.message( 'deputy.cte.splitArticle.entry.remove' ).text(), + framed: false, + flags: [ 'destructive' ] + } ); + deleteButton.on( 'click', () => { + this.splitArticleTemplateRow.parent.deleteRow( this.splitArticleTemplateRow ); + } ); + + return
+ { unwrapWidget( deleteButton )} +
; + } + + /** + * Renders a set of OOUI InputWidgets and FieldLayouts, eventually returning an + * array of each FieldLayout to append to the FieldsetLayout. + * + * @return An array of OOUI FieldLayouts + */ + renderFields(): any[] { + const rowDate = this.splitArticleTemplateRow.date; + const parsedDate = + ( rowDate == null || rowDate.trim().length === 0 ) ? + undefined : ( + !isNaN( new Date( rowDate.trim() + ' UTC' ).getTime() ) ? + ( new Date( rowDate.trim() + ' UTC' ) ) : ( + !isNaN( new Date( rowDate.trim() ).getTime() ) ? + new Date( rowDate.trim() ) : null + ) + ); + + this.inputs = { + to: new mw.widgets.TitleInputWidget( { + $overlay: this.parent.$overlay, + required: true, + value: this.splitArticleTemplateRow.to || '', + placeholder: mw.message( 'deputy.cte.splitArticle.to.placeholder' ).text() + } ), + // eslint-disable-next-line camelcase + from_oldid: new OO.ui.TextInputWidget( { + $overlay: this.parent.$overlay, + value: this.splitArticleTemplateRow.from_oldid || '', + placeholder: mw.message( 'deputy.cte.splitArticle.to.placeholder' ).text() + } ), + date: new mw.widgets.datetime.DateTimeInputWidget( { + $overlay: this.parent.$overlay, + required: true, + calendar: null, + icon: 'calendar', + clearable: true, + value: parsedDate + } ), + diff: new OO.ui.TextInputWidget( { + $overlay: this.parent.$overlay, + value: this.splitArticleTemplateRow.from_oldid || '', + placeholder: mw.message( 'deputy.cte.splitArticle.diff.placeholder' ).text(), + validate: ( value: string ) => { + if ( value.trim().length === 0 ) { + return true; + } + try { + return typeof new URL( value ).href === 'string'; + } catch ( e ) { + return false; + } + } + } ) + }; + this.fieldLayouts = { + to: new OO.ui.FieldLayout( this.inputs.to, { + $overlay: this.parent.$overlay, + align: 'top', + label: mw.message( 'deputy.cte.splitArticle.to.label' ).text(), + help: mw.message( 'deputy.cte.splitArticle.to.help' ).text() + } ), + // eslint-disable-next-line camelcase + from_oldid: new OO.ui.FieldLayout( this.inputs.from_oldid, { + $overlay: this.parent.$overlay, + align: 'left', + label: mw.message( 'deputy.cte.splitArticle.from_oldid.label' ).text(), + help: mw.message( 'deputy.cte.splitArticle.from_oldid.help' ).text() + } ), + date: new OO.ui.FieldLayout( this.inputs.date, { + $overlay: this.parent.$overlay, + align: 'left', + label: mw.message( 'deputy.cte.splitArticle.date.label' ).text(), + help: mw.message( 'deputy.cte.splitArticle.date.help' ).text() + } ), + diff: new OO.ui.FieldLayout( this.inputs.diff, { + $overlay: this.parent.$overlay, + align: 'left', + label: mw.message( 'deputy.cte.splitArticle.diff.label' ).text(), + help: mw.message( 'deputy.cte.splitArticle.diff.help' ).text() + } ) + }; + + for ( const _field in this.inputs ) { + const field = _field as SplitArticleTemplateRowParameter; + const input = this.inputs[ field ]; + + // Attach the change listener + input.on( 'change', ( value: string ) => { + if ( input instanceof mw.widgets.datetime.DateTimeInputWidget ) { + this.splitArticleTemplateRow[ field ] = + new Date( value ).toLocaleDateString( 'en-GB', { + year: 'numeric', month: 'long', day: 'numeric' + } ); + if ( value.length > 0 ) { + this.fieldLayouts[ field ].setWarnings( [] ); + } + } else { + this.splitArticleTemplateRow[ field ] = value; + } + this.splitArticleTemplateRow.parent.save(); + } ); + + if ( input instanceof OO.ui.TextInputWidget ) { + // Rechecks the validity of the field. + input.setValidityFlag(); + } + } + + this.inputs.to.on( 'change', () => { + this.outlineItem.setLabel( + `${ + this.splitArticleTemplateRow.to || '???' + } on ${this.splitArticleTemplateRow.date || '???'}` + ); + } ); + this.inputs.date.on( 'change', () => { + this.outlineItem.setLabel( + `${ + this.splitArticleTemplateRow.to || '???' + } on ${this.splitArticleTemplateRow.date || '???'}` + ); + } ); + + return getObjectValues( this.fieldLayouts ); + } + /** * Sets up the outline item of this page. Used in the BookletLayout. */