File: ve.ui.MWExtensionWindow.js

package info (click to toggle)
mediawiki 1%3A1.43.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 417,464 kB
  • sloc: php: 1,062,949; javascript: 664,290; sql: 9,714; python: 5,458; xml: 3,489; sh: 1,131; makefile: 64
file content (309 lines) | stat: -rw-r--r-- 8,912 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/*!
 * VisualEditor UserInterface MWExtensionWindow class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * Mixin for windows for editing generic MediaWiki extensions.
 *
 * @class
 * @abstract
 *
 * @constructor
 * @param {Object} [config] Configuration options
 */
ve.ui.MWExtensionWindow = function VeUiMWExtensionWindow() {
	this.whitespace = null;
	this.input = null;
	this.originalMwData = null;

	this.onChangeHandler = ve.debounce( this.onChange.bind( this ) );
};

/* Inheritance */

OO.initClass( ve.ui.MWExtensionWindow );

/* Static properties */

/**
 * Extension is allowed to have empty contents
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.ui.MWExtensionWindow.static.allowedEmpty = false;

/**
 * Tell Parsoid to self-close tags when the body is empty
 *
 * i.e. `<foo></foo>` -> `<foo/>`
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.ui.MWExtensionWindow.static.selfCloseEmptyBody = false;

/**
 * Inspector's directionality, 'ltr' or 'rtl'
 *
 * Leave as null to use the directionality of the current fragment.
 *
 * @static
 * @property {string|null}
 * @inheritable
 */
ve.ui.MWExtensionWindow.static.dir = null;

/* Methods */

/**
 * @inheritdoc OO.ui.Window
 */
ve.ui.MWExtensionWindow.prototype.initialize = function () {
	this.input = new ve.ui.WhitespacePreservingTextInputWidget( {
		limit: 1,
		classes: [ 've-ui-mwExtensionWindow-input' ]
	} );
};

/**
 * Get the placeholder text for the content input area.
 *
 * @return {string} Placeholder text
 */
ve.ui.MWExtensionWindow.prototype.getInputPlaceholder = function () {
	return '';
};

/**
 * @inheritdoc OO.ui.Window
 */
ve.ui.MWExtensionWindow.prototype.getSetupProcess = function ( data, process ) {
	data = data || {};
	return process.next( () => {
		// Initialization
		this.whitespace = [ '', '' ];

		if ( this.selectedNode ) {
			const mwData = this.selectedNode.getAttribute( 'mw' );
			// mwData.body can be null in <selfclosing/> extensions
			this.input.setValueAndWhitespace( ( mwData.body && mwData.body.extsrc ) || '' );
			this.originalMwData = mwData;
		} else {
			if ( !this.constructor.static.modelClasses[ 0 ].static.isContent ) {
				// New nodes should use linebreaks for blocks
				this.input.setWhitespace( [ '\n', '\n' ] );
			}
			this.input.setValue( '' );
		}

		this.input.$input.attr( 'placeholder', this.getInputPlaceholder() );

		const dir = this.constructor.static.dir || data.dir;
		this.input.setDir( dir );
		this.input.setReadOnly( this.isReadOnly() );

		this.actions.setAbilities( { done: false } );
		this.input.connect( this, { change: 'onChangeHandler' } );
	} );
};

/**
 * @inheritdoc OO.ui.Window
 */
ve.ui.MWExtensionWindow.prototype.getReadyProcess = function ( data, process ) {
	return process;
};

/**
 * @inheritdoc OO.ui.Window
 */
ve.ui.MWExtensionWindow.prototype.getTeardownProcess = function ( data, process ) {
	return process.next( () => {
		// Don't hold on to the original data, it's only refreshed on setup for existing nodes
		this.originalMwData = null;
		this.input.disconnect( this, { change: 'onChangeHandler' } );
	} );
};

/**
 * @inheritdoc OO.ui.Dialog
 */
ve.ui.MWExtensionWindow.prototype.getActionProcess = function ( action, process ) {
	return process.first( () => {
		if ( action === 'done' ) {
			if ( this.constructor.static.allowedEmpty || this.input.getValue() !== '' ) {
				this.insertOrUpdateNode();
			} else if ( this.selectedNode && !this.constructor.static.allowedEmpty ) {
				// Content has been emptied on a node which isn't allowed to
				// be empty, so delete it.
				this.removeNode();
			}
		}
	} );
};

/**
 * Handle change event.
 */
ve.ui.MWExtensionWindow.prototype.onChange = function () {
	this.updateActions();
};

/**
 * Update the 'done' action according to whether there are changes
 */
ve.ui.MWExtensionWindow.prototype.updateActions = function () {
	this.actions.setAbilities( { done: this.isSaveable() } );
};

/**
 * Check if mwData would be modified if window contents were applied.
 * This is used to determine if it's meaningful for the user to save the
 * contents into the document; this is likely true of newly-created elements.
 *
 * @return {boolean} mwData would be modified
 */
ve.ui.MWExtensionWindow.prototype.isSaveable = function () {
	let modified;
	if ( this.originalMwData ) {
		const mwDataCopy = ve.copy( this.originalMwData );
		this.updateMwData( mwDataCopy );
		modified = !ve.compare( this.originalMwData, mwDataCopy );
	} else {
		modified = true;
	}
	return modified;
};

/**
 * @deprecated Moved to ve.ui.MWExtensionWindow.prototype.isSaveable
 * @return {boolean} mwData would be modified
 */
ve.ui.MWExtensionWindow.prototype.isModified = ve.ui.MWExtensionWindow.prototype.isSaveable;

/**
 * Check if mwData has meaningful edits. This is used to determine if it's
 * meaningful to warn the user before closing the dialog without saving. Unlike
 * `isModified()` above, we consider a newly-created but unmodified element to
 * be non-meaningful because the user can simply re-open the dialog to restore
 * their state.
 *
 * @return {boolean} mwData would contain new user input
 */
ve.ui.MWExtensionWindow.prototype.hasMeaningfulEdits = function () {
	let mwDataBaseline;
	if ( this.originalMwData ) {
		mwDataBaseline = this.originalMwData;
	} else {
		mwDataBaseline = this.getNewElement().attributes.mw;
	}
	const mwDataCopy = ve.copy( mwDataBaseline );
	this.updateMwData( mwDataCopy );

	// We have some difficulty here. `updateMwData()` in this class calls on
	// `this.input.getValueAndWhitespace()`. The 'and whitespace' means that
	// we cannot directly compare a new element's mwData with a newly-opened
	// dialog's mwData because it may have additional newlines.
	// We don't want to touch `this.input` or `prototype.updateMwData` because
	// they're overridden in subclasses. Therefore, we consider whitespace-only
	// changes to a new element to be non-meaningful too.
	const changed = OO.getProp( mwDataCopy, 'body', 'extsrc' );
	if ( changed !== undefined ) {
		OO.setProp( mwDataCopy, 'body', 'extsrc', changed.trim() );
	}

	// Also trim the baseline. In "edit" mode we likely have added whitespace,
	// and in "insert" mode we don't want to break if the default value starts
	// or ends with whitespace.
	const baselineChanged = OO.getProp( mwDataBaseline, 'body', 'extsrc' );
	if ( baselineChanged !== undefined ) {
		OO.setProp( mwDataBaseline, 'body', 'extsrc', baselineChanged.trim() );
	}

	return !ve.compare( mwDataBaseline, mwDataCopy );
};

/**
 * Create an new data element for the model class associated with this inspector
 *
 * @return {Object} Element data
 */
ve.ui.MWExtensionWindow.prototype.getNewElement = function () {
	// Extension inspectors which create elements should either match
	// a single modelClass or override this method.
	const modelClass = this.constructor.static.modelClasses[ 0 ];
	return {
		type: modelClass.static.name,
		attributes: {
			mw: {
				name: modelClass.static.extensionName,
				attrs: {},
				body: {
					extsrc: ''
				}
			}
		}
	};
};

/**
 * Insert or update the node in the document model from the new values
 */
ve.ui.MWExtensionWindow.prototype.insertOrUpdateNode = function () {
	const surfaceModel = this.getFragment().getSurface();
	if ( this.selectedNode ) {
		const mwData = ve.copy( this.selectedNode.getAttribute( 'mw' ) );
		this.updateMwData( mwData );
		surfaceModel.change(
			ve.dm.TransactionBuilder.static.newFromAttributeChanges(
				surfaceModel.getDocument(),
				this.selectedNode.getOuterRange().start,
				{ mw: mwData }
			)
		);
	} else {
		const element = this.getNewElement();
		this.updateMwData( element.attributes.mw );
		// Collapse returns a new fragment, so update this.fragment
		this.fragment = this.getFragment().collapseToEnd();
		this.getFragment().insertContent( [
			element,
			{ type: '/' + element.type }
		] );
	}
};

/**
 * Remove the node form the document model
 */
ve.ui.MWExtensionWindow.prototype.removeNode = function () {
	this.getFragment().removeContent();
};

/**
 * Update mwData object with the new values from the inspector or dialog
 *
 * @param {Object} mwData MediaWiki data object
 */
ve.ui.MWExtensionWindow.prototype.updateMwData = function ( mwData ) {
	const tagName = mwData.name;
	let value = this.input.getValueAndWhitespace();

	// XML-like tags in wikitext are not actually XML and don't expect their contents to be escaped.
	// This means that it is not possible for a tag '<foo>…</foo>' to contain the string '</foo>'.
	// Prevent that by escaping the first angle bracket '<' to '&lt;'. (T59429)
	value = value.replace( new RegExp( '<(/' + tagName + '\\s*>)', 'gi' ), '&lt;$1' );

	if ( value.trim() === '' && this.constructor.static.selfCloseEmptyBody ) {
		delete mwData.body;
	} else {
		mwData.body = mwData.body || {};
		mwData.body.extsrc = value;
	}
};