Completed
Push — master ( eadb3e...25e675 )
by
unknown
10:12
created

ModalBuilder::generateHeader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 24
cts 24
cp 1
rs 9.44
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * Contains the class holding a modal building kit.
4
 *
5
 * @copyright (C) 2018, Tobias Oetterer, Paderborn University
6
 * @license       https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License, version 3 (or later)
7
 *
8
 * This file is part of the MediaWiki extension BootstrapComponents.
9
 * The BootstrapComponents extension is free software: you can redistribute it
10
 * and/or modify it under the terms of the GNU General Public License as published
11
 * by the Free Software Foundation, either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * The BootstrapComponents extension is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
21
 *
22
 * @file
23
 * @ingroup       BootstrapComponents
24
 * @author        Tobias Oetterer
25
 */
26
27
namespace BootstrapComponents;
28
29
use \Html;
30
31
/**
32
 * Class ModalBase
33
 *
34
 * This is a low layer class, that helps build a modal. It does not have access to a parser, so it expects all content
35
 * elements to be hardened by you (with the help of {@see Parser::recursiveTagParse}). All attribute elements
36
 * will be hardened here, through the use of {@see Html::rawElement}.
37
 *
38
 * Tested hooks to insert the deferred content:
39
 * * ParserBeforeTidy: Injects content anytime a parser is invoked. Including for instance the searchGoButton... :(
40
 * * ParserAfterTidy: Same as ParserBeforeTidy
41
 * * SkinAfterContent: Works; content inside mw-data-after-content container, which trails the "mw-content-text" and the "printfooter" divs.
42
 *      Needs deferred content to be stored in parser cache. This runs into problems with fixed-head under chameleon 1.7.0+!
43
 * * OutputPageParserOutput: Works; adds either at the bottom of the content (right after the tidy remarks and the comment containing caching information)
44
 *      or at the top (at the start of the content div, direct after the "body text" comment); both options within the "mw-content-text" div container.
45
 *      Needs deferred content to be stored in parser cache. This runs into problems with fixed-head under chameleon 1.7.0+!
46
 *
47
 * @since 1.0
48
 */
49
class ModalBuilder {
50
51
	/**
52
	 * @var string $content
53
	 */
54
	private $content;
55
56
	/**
57
	 * @var string|false $footer
58
	 */
59
	private $footer;
60
61
	/**
62
	 * @var string|false $header
63
	 */
64
	private $header;
65
66
	/**
67
	 * @var string $id
68
	 */
69
	private $id;
70
71
	/**
72
	 * @var string|false $bodyClass
73
	 */
74
	private $bodyClass;
75
76
	/**
77
	 * @var string|false $bodyStyle
78
	 */
79
	private $bodyStyle;
80
81
	/**
82
	 * @var string|false $dialogClass
83
	 */
84
	private $dialogClass;
85
86
	/**
87
	 * @var string|false $dialogStyle
88
	 */
89
	private $dialogStyle;
90
91
	/**
92
	 * @var string|false $outerClass
93
	 */
94
	private $outerClass;
95
96
	/**
97
	 * @var string|false $outerStyle
98
	 */
99
	private $outerStyle;
100
101
	/**
102
	 * @var ParserOutputHelper $parserOutputHelper
103
	 */
104
	private $parserOutputHelper;
105
106
	/**
107
	 * @var string $trigger
108
	 */
109
	private $trigger;
110
111
	/**
112
	 * With this, you can wrap a generic trigger element inside a span block, that hopefully should
113
	 * work as a trigger for the modal
114
	 *
115
	 * @param string $element
116
	 * @param string $id
117
	 *
118
	 * @return string
119
	 */
120 7
	public static function wrapTriggerElement( $element, $id ) {
121 7
		return Html::rawElement(
122 7
			'span',
123
			[
124 7
				'class'       => 'modal-trigger',
125 7
				'data-toggle' => 'modal',
126 7
				'data-target' => '#' . $id,
127 7
			],
128
			$element
129 7
		);
130
	}
131
132
	/**
133
	 * ModalBase constructor.
134
	 *
135
	 * Takes $id, $trigger and $content and produces a modal with the html id $id, using $content as the
136
	 * body content of the opening modal. For trigger, you can use a generic html code and wrap it in
137
	 * {@see \BootstrapComponents\ModalBase::wrapTriggerElement}, or you make sure you generate
138
	 * a correct trigger for yourself, using the necessary attributes and especially the id, you supplied
139
	 * here (see {@see \BootstrapComponents\Component\Modal::generateButton} for example).
140
	 *
141
	 * Do not instantiate directly, but use {@see ApplicationFactory::getModalBuilder}
142
	 * instead.
143
	 *
144
	 * @see ApplicationFactory::getModalBuilder
145
	 * @see \BootstrapComponents\Component\Modal::generateButton
146
	 *
147
	 * @param string             $id
148
	 * @param string             $trigger must be safe raw html (best run through {@see Parser::recursiveTagParse})
149
	 * @param string             $content must be fully parsed html (use {@see Parser::recursiveTagParseFully})
150
	 * @param ParserOutputHelper $parserOutputHelper
151
	 */
152
153 9
	public function __construct( $id, $trigger, $content, $parserOutputHelper ) {
154 9
		$this->id = $id;
155 9
		$this->trigger = $trigger;
156 9
		$this->content = $content;
157 9
		$this->parserOutputHelper = $parserOutputHelper;
158 9
	}
159
160
	/**
161
	 * Parses the modal.
162
	 *
163
	 * @return string
164
	 */
165 8
	public function parse() {
166 8
		$this->parserOutputHelper->injectLater(
167 8
			$this->getId(),
168 8
			$this->buildModal()
169 8
		);
170 8
		return $this->buildTrigger();
171
	}
172
173
	/**
174
	 * Lets you set the class attribute for the modal body. The body is the element holding the content.
175
	 *
176
	 * @param string|false $bodyClass
177
	 *
178
	 * @return ModalBuilder
179
	 */
180
	public function setBodyClass( $bodyClass ) {
181
		$this->bodyClass = $bodyClass;
182
		return $this;
183
	}
184
185
	/**
186
	 * Lets you set the style attribute for the modal body. The body is the element holding the content.
187
	 *
188
	 * @param string|false $bodyStyle
189
	 *
190
	 * @return ModalBuilder
191
	 */
192
	public function setBodyStyle( $bodyStyle ) {
193
		$this->bodyStyle = $bodyStyle;
194
		return $this;
195
	}
196
197
	/**
198
	 * Lets you set the class attribute for the dialog part. The dialog is the container, holding header, body, and footer.
199
	 * The dialog is surrounded by the outer modal. See {@see ModalBuilder::setOuterClass}.
200
	 *
201
	 * @param string|false $dialogClass
202
	 *
203
	 * @return ModalBuilder
204
	 */
205 3
	public function setDialogClass( $dialogClass ) {
206 3
		$this->dialogClass = $dialogClass;
207 3
		return $this;
208
	}
209
210
	/**
211
	 * Lets you set the style attribute for the dialog part. The dialog is the container, holding header, body, and footer.
212
	 * The dialog is surrounded by the outer modal. See {@see ModalBuilder::setOuterClass}.
213
	 *
214
	 * @param string|false $dialogStyle
215
	 *
216
	 * @return ModalBuilder
217
	 */
218
	public function setDialogStyle( $dialogStyle ) {
219
		$this->dialogStyle = $dialogStyle;
220
		return $this;
221
	}
222
223
	/**
224
	 * Sets the content for the footer section.
225
	 *
226
	 * @param string|false $footer must be safe raw html (best run through {@see Parser::recursiveTagParse})
227
	 *
228
	 * @return ModalBuilder
229
	 */
230 7
	public function setFooter( $footer ) {
231 7
		$this->footer = $footer;
232 7
		return $this;
233
	}
234
235
	/**
236
	 * Sets the content for the header section.
237
	 *
238
	 * @param string|false $header must be safe raw html (best run through {@see Parser::recursiveTagParse})
239
	 *
240
	 * @return ModalBuilder
241
	 */
242 7
	public function setHeader( $header ) {
243 7
		$this->header = $header;
244 7
		return $this;
245
	}
246
247
	/**
248
	 * Lets you set the class attribute for the outermost modal container.
249
	 *
250
	 * @param string|false $outerClass
251
	 *
252
	 * @return ModalBuilder
253
	 */
254 3
	public function setOuterClass( $outerClass ) {
255 3
		$this->outerClass = $outerClass;
256 3
		return $this;
257
	}
258
259
	/**
260
	 * Lets you set the style attribute for the outermost modal container.
261
	 *
262
	 * @param string|false $outerStyle
263
	 *
264
	 * @return ModalBuilder
265
	 */
266 3
	public function setOuterStyle( $outerStyle ) {
267 3
		$this->outerStyle = $outerStyle;
268 3
		return $this;
269
	}
270
271
	/**
272
	 * From all the data passed by caller, this builds the dialog part (the one inside the modal holding the actual content).
273
	 *
274
	 * @return string
275
	 */
276 8
	protected function buildDialog() {
277 8
		return Html::rawElement(
278 8
			'div',
279
			[
280 8
				'class' => $this->compileClass(
281 8
					'modal-dialog',
282 8
					$this->getDialogClass()
283 8
				),
284 8
				'style' => $this->getDialogStyle(),
285 8
			],
286 8
			Html::rawElement(
287 8
				'div',
288 8
				[ 'class' => 'modal-content' ],
289 8
				$this->generateHeader(
290 8
					$this->getHeader()
291 8
				)
292 8
				. $this->generateBody(
293 8
					$this->getContent()
294 8
				)
295 8
				. $this->generateFooter(
296 8
					$this->getFooter()
297 8
				)
298 8
			)
299 8
		);
300
	}
301
302
	/**
303
	 * From all the data passed by caller, this builds the dialog part (the one that pops up when engaging the trigger).
304
	 *
305
	 * @return string
306
	 */
307 8
	protected function buildModal() {
308 8
		return Html::rawElement(
309 8
			'div',
310
			[
311 8
				'class'       => $this->compileClass(
312 8
					'modal fade',
313 8
					$this->getOuterClass()
314 8
				),
315 8
				'style'       => $this->getOuterStyle(),
316 8
				'role'        => 'dialog',
317 8
				'id'          => $this->getId(),
318 8
				'aria-hidden' => 'true',
319 8
			],
320 8
			$this->buildDialog()
321 8
		) . "\n";
322
	}
323
324
	/**
325
	 * Performs the necessary steps to convert the string passed by caller into a working trigger for the modal.
326
	 *
327
	 * @return string
328
	 */
329 8
	protected function buildTrigger() {
330 8
		$trigger = $this->getTrigger();
331 8
		if ( preg_match( '/data-toggle[^"]+"modal/', $trigger )
332 8
			&& preg_match( '/data-target[^"]+"#' . $this->getId() . '"/', $trigger )
333 8
			&& preg_match( '/class[^"]+"[^"]*modal-trigger' . '/', $trigger )
334 8
		) {
335 6
			return $trigger;
336
		}
337 2
		return self::wrapTriggerElement( $trigger, $this->getId() );
338
	}
339
340
	/**
341
	 * Used to merge different class attributes.
342
	 *
343
	 * @param string       $baseClass
344
	 * @param string|false $additionalClass
345
	 *
346
	 * @return string
347
	 */
348 8
	protected function compileClass( $baseClass, $additionalClass ) {
349 8
		if ( trim( $additionalClass ) ) {
350 2
			return $baseClass . ' ' . trim( $additionalClass );
351
		}
352 8
		return $baseClass;
353
	}
354
355
	/**
356
	 * @return string|false
357
	 */
358 8
	protected function getBodyClass() {
359 8
		return $this->bodyClass;
360
	}
361
362
	/**
363
	 * @return string|false
364
	 */
365 8
	protected function getBodyStyle() {
366 8
		return $this->bodyStyle;
367
	}
368
369
	/**
370
	 * Build the modal, with all sections, requested content and necessary control elements.
371
	 *
372
	 * @return string
373
	 */
374 8
	protected function getContent() {
375 8
		return $this->content;
376
	}
377
378
	/**
379
	 * @return string|false
380
	 */
381 8
	protected function getDialogClass() {
382 8
		return $this->dialogClass;
383
	}
384
385
	/**
386
	 * @return string|false
387
	 */
388 8
	protected function getDialogStyle() {
389 8
		return $this->dialogStyle;
390
	}
391
392
	/**
393
	 * @return string|false
394
	 */
395 8
	protected function getFooter() {
396 8
		return $this->footer;
397
	}
398
399
	/**
400
	 * @return string|false
401
	 */
402 8
	protected function getHeader() {
403 8
		return $this->header;
404
	}
405
406
	/**
407
	 * @return string
408
	 */
409 8
	protected function getId() {
410 8
		return $this->id;
411
	}
412
413
	/**
414
	 * @return string|false
415
	 */
416 8
	protected function getOuterClass() {
417 8
		return $this->outerClass;
418
	}
419
420
	/**
421
	 * @return string|false
422
	 */
423 8
	protected function getOuterStyle() {
424 8
		return $this->outerStyle;
425
	}
426
427
	/**
428
	 * Returns the supplied trigger. Wraps it with {@see ModalBuilder::wrapTriggerElement} if certain attributes are not detected.
429
	 *
430
	 * @return string
431
	 */
432 8
	protected function getTrigger() {
433 8
		return $this->trigger;
434
	}
435
436
	/**
437
	 * Generates the body section.
438
	 *
439
	 * @param string $content must be safe raw html (best run through {@see Parser::recursiveTagParse})
440
	 *
441
	 * @return string
442
	 */
443 8
	private function generateBody( $content ) {
444 8
		return Html::rawElement(
445 8
				'div',
446
				[
447 8
					'class' => $this->compileClass( 'modal-body', $this->getBodyClass() ),
448 8
					'style' => $this->getBodyStyle(),
449 8
				],
450
				$content
451 8
			);
452
	}
453
454
	/**
455
	 * Generates the footer section.
456
	 *
457
	 * @param string|false $footer must be safe raw html (best run through {@see Parser::recursiveTagParse})
458
	 *
459
	 * @return string
460
	 */
461 8
	private function generateFooter( $footer = '' ) {
462 8
		if ( empty( $footer ) ) {
463 3
			$footer = '';
464 3
		}
465 8
		$close = wfMessage( 'bootstrap-components-close-element' )->inContentLanguage()->text();
466 8
		return Html::rawElement(
467 8
				'div',
468 8
				[ 'class' => 'modal-footer' ],
469 8
				$footer . Html::rawElement(
470 8
					'button',
471
					[
472 8
						'type'         => 'button',
473 8
						'class'        => 'btn btn-default',
474 8
						'data-dismiss' => 'modal',
475 8
						'aria-label'   => $close,
476 8
					],
477
					$close
478 8
				)
479 8
			);
480
	}
481
482
	/**
483
	 * Generates the header section together with the dismiss X and the heading, if provided.
484
	 *
485
	 * @param string|false $header must be safe raw html (best run through {@see Parser::recursiveTagParse})
486
	 *
487
	 * @return string
488
	 */
489 8
	private function generateHeader( $header = '' ) {
490 8
		if ( empty( $header ) ) {
491 3
			$header = '';
492 3
		} else {
493 6
			$header = Html::rawElement(
494 6
				'span',
495 6
				[ 'class' => 'modal-title' ],
496
				$header
497 6
			);
498
		}
499 8
		$button = Html::rawElement(
500 8
			'button',
501
			[
502 8
				'type'         => 'button',
503 8
				'class'        => 'close',
504 8
				'data-dismiss' => 'modal',
505 8
				'aria-label'   => wfMessage( 'bootstrap-components-close-element' )->inContentLanguage()->text(),
506 8
			],
507 8
			Html::rawElement(
508 8
				'span',
509 8
				[ 'aria-hidden' => 'true' ],
510
				'&times;'
511 8
			)
512 8
		);
513 8
		return Html::rawElement(
514 8
				'div',
515 8
				[ 'class' => 'modal-header' ],
516
				$button 	. $header
517 8
			);
518
	}
519
}