]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - vendor/oojs/oojs-ui/demos/demo.js
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / oojs / oojs-ui / demos / demo.js
1 /* eslint-disable no-console */
2 /* globals Prism, javascriptStringify */
3 /**
4  * @class
5  * @extends OO.ui.Element
6  *
7  * @constructor
8  */
9 window.Demo = function Demo() {
10         var demo = this;
11
12         // Parent constructor
13         Demo.parent.call( this );
14
15         // Mixin constructors
16         OO.EventEmitter.call( this );
17
18         // Normalization
19         this.normalizeQuery();
20
21         // Properties
22         this.stylesheetLinks = this.getStylesheetLinks();
23         this.mode = this.getCurrentMode();
24         this.$menu = $( '<div>' );
25         this.pageDropdown = new OO.ui.DropdownWidget( {
26                 menu: {
27                         items: [
28                                 new OO.ui.MenuOptionWidget( { data: 'dialogs', label: 'Dialogs' } ),
29                                 new OO.ui.MenuOptionWidget( { data: 'icons', label: 'Icons' } ),
30                                 new OO.ui.MenuOptionWidget( { data: 'toolbars', label: 'Toolbars' } ),
31                                 new OO.ui.MenuOptionWidget( { data: 'widgets', label: 'Widgets' } )
32                         ]
33                 },
34                 classes: [ 'demo-pageDropdown' ]
35         } );
36         this.pageMenu = this.pageDropdown.getMenu();
37         this.themeSelect = new OO.ui.ButtonSelectWidget();
38         Object.keys( this.constructor.static.themes ).forEach( function ( theme ) {
39                 demo.themeSelect.addItems( [
40                         new OO.ui.ButtonOptionWidget( {
41                                 data: theme,
42                                 label: demo.constructor.static.themes[ theme ]
43                         } )
44                 ] );
45         } );
46         this.directionSelect = new OO.ui.ButtonSelectWidget().addItems( [
47                 new OO.ui.ButtonOptionWidget( { data: 'ltr', label: 'LTR' } ),
48                 new OO.ui.ButtonOptionWidget( { data: 'rtl', label: 'RTL' } )
49         ] );
50         this.jsPhpSelect = new OO.ui.ButtonGroupWidget().addItems( [
51                 new OO.ui.ButtonWidget( { label: 'JS' } ).setActive( true ),
52                 new OO.ui.ButtonWidget( {
53                         label: 'PHP',
54                         href: 'demos.php' + this.getUrlQuery( this.getCurrentFactorValues() )
55                 } )
56         ] );
57         this.platformSelect = new OO.ui.ButtonSelectWidget().addItems( [
58                 new OO.ui.ButtonOptionWidget( { data: 'desktop', label: 'Desktop' } ),
59                 new OO.ui.ButtonOptionWidget( { data: 'mobile', label: 'Mobile' } )
60         ] );
61
62         this.documentationLink = new OO.ui.ButtonWidget( {
63                 label: 'Docs',
64                 classes: [ 'demo-button-docs' ],
65                 icon: 'journal',
66                 href: '../js/',
67                 flags: [ 'progressive' ]
68         } );
69
70         // Events
71         this.pageMenu.on( 'choose', OO.ui.bind( this.onModeChange, this ) );
72         this.themeSelect.on( 'choose', OO.ui.bind( this.onModeChange, this ) );
73         this.directionSelect.on( 'choose', OO.ui.bind( this.onModeChange, this ) );
74         this.platformSelect.on( 'choose', OO.ui.bind( this.onModeChange, this ) );
75
76         // Initialization
77         this.pageMenu.selectItemByData( this.mode.page );
78         this.themeSelect.selectItemByData( this.mode.theme );
79         this.directionSelect.selectItemByData( this.mode.direction );
80         this.platformSelect.selectItemByData( this.mode.platform );
81         this.$menu
82                 .addClass( 'demo-menu' )
83                 .attr( 'role', 'navigation' )
84                 .append(
85                         this.pageDropdown.$element,
86                         this.themeSelect.$element,
87                         this.directionSelect.$element,
88                         this.jsPhpSelect.$element,
89                         this.platformSelect.$element,
90                         this.documentationLink.$element
91                 );
92         this.$element
93                 .addClass( 'demo' )
94                 .append( this.$menu );
95         $( 'html' ).attr( 'dir', this.mode.direction );
96         $( 'head' ).append( this.stylesheetLinks );
97         // eslint-disable-next-line new-cap
98         OO.ui.theme = new OO.ui[ this.constructor.static.themes[ this.mode.theme ] + 'Theme' ]();
99         OO.ui.isMobile = function () {
100                 return demo.mode.platform === 'mobile';
101         };
102 };
103
104 /* Setup */
105
106 OO.inheritClass( Demo, OO.ui.Element );
107 OO.mixinClass( Demo, OO.EventEmitter );
108
109 /* Static Properties */
110
111 /**
112  * Available pages.
113  *
114  * Populated by each of the page scripts in the `pages` directory.
115  *
116  * @static
117  * @property {Object.<string,Function>} pages List of functions that render a page, keyed by
118  *   symbolic page name
119  */
120 Demo.static.pages = {};
121
122 /**
123  * Available themes.
124  *
125  * Map of lowercase name to proper name. Lowercase names are used for linking to the
126  * correct stylesheet file. Proper names are used to find the theme class.
127  *
128  * @static
129  * @property {Object.<string,string>}
130  */
131 Demo.static.themes = {
132         wikimediaui: 'WikimediaUI', // Do not change this line or you'll break `grunt add-theme`
133         apex: 'Apex'
134 };
135
136 /**
137  * Additional suffixes for which each theme defines image modules.
138  *
139  * @static
140  * @property {Object.<string,string[]>
141  */
142 Demo.static.additionalThemeImagesSuffixes = {
143         wikimediaui: [
144                 '-icons-movement',
145                 '-icons-content',
146                 '-icons-alerts',
147                 '-icons-interactions',
148                 '-icons-moderation',
149                 '-icons-editing-core',
150                 '-icons-editing-styling',
151                 '-icons-editing-list',
152                 '-icons-editing-advanced',
153                 '-icons-media',
154                 '-icons-location',
155                 '-icons-user',
156                 '-icons-layout',
157                 '-icons-accessibility',
158                 '-icons-wikimedia'
159         ],
160         apex: [
161                 '-icons-movement',
162                 '-icons-content',
163                 '-icons-alerts',
164                 '-icons-interactions',
165                 '-icons-moderation',
166                 '-icons-editing-core',
167                 '-icons-editing-styling',
168                 '-icons-editing-list',
169                 '-icons-editing-advanced',
170                 '-icons-media',
171                 '-icons-user',
172                 '-icons-layout',
173                 '-icons-accessibility'
174         ]
175 };
176
177 /**
178  * Available text directions.
179  *
180  * List of text direction descriptions, each containing a `fileSuffix` property used for linking to
181  * the correct stylesheet file.
182  *
183  * @static
184  * @property {Object.<string,Object>}
185  */
186 Demo.static.directions = {
187         ltr: { fileSuffix: '' },
188         rtl: { fileSuffix: '.rtl' }
189 };
190
191 /**
192  * Available platforms.
193  *
194  * @static
195  * @property {string[]}
196  */
197 Demo.static.platforms = [ 'desktop', 'mobile' ];
198
199 /**
200  * Default page.
201  *
202  * @static
203  * @property {string}
204  */
205 Demo.static.defaultPage = 'widgets';
206
207 /**
208  * Default page.
209  *
210  * Set by one of the page scripts in the `pages` directory.
211  *
212  * @static
213  * @property {string}
214  */
215 Demo.static.defaultTheme = 'wikimediaui';
216
217 /**
218  * Default page.
219  *
220  * Set by one of the page scripts in the `pages` directory.
221  *
222  * @static
223  * @property {string}
224  */
225 Demo.static.defaultDirection = 'ltr';
226
227 /**
228  * Default platform.
229  *
230  * Set by one of the page scripts in the `pages` directory.
231  *
232  * @static
233  * @property {string}
234  */
235 Demo.static.defaultPlatform = 'desktop';
236
237 /* Static Methods */
238
239 /**
240  * Scroll to current fragment identifier. We have to do this manually because of the fixed header.
241  */
242 Demo.static.scrollToFragment = function () {
243         var elem = document.getElementById( location.hash.slice( 1 ) );
244         if ( elem ) {
245                 // The additional '10' is just because it looks nicer.
246                 $( window ).scrollTop( $( elem ).offset().top - $( '.demo-menu' ).outerHeight() - 10 );
247         }
248 };
249
250 /* Methods */
251
252 /**
253  * Load the demo page. Must be called after $element is attached.
254  *
255  * @return {jQuery.Promise} Resolved when demo is initialized
256  */
257 Demo.prototype.initialize = function () {
258         var demo = this,
259                 promises = this.stylesheetLinks.map( function ( el ) {
260                         return $( el ).data( 'load-promise' );
261                 } );
262
263         // Helper function to get high resolution profiling data, where available.
264         function now() {
265                 return ( window.performance && performance.now ) ? performance.now() :
266                         Date.now ? Date.now() : new Date().getTime();
267         }
268
269         return $.when.apply( $, promises )
270                 .done( function () {
271                         var start, end;
272                         start = now();
273                         demo.constructor.static.pages[ demo.mode.page ]( demo );
274                         end = now();
275                         window.console.log( 'Took ' + ( end - start ) + ' ms to build demo page.' );
276                 } )
277                 .fail( function () {
278                         demo.$element.append( $( '<p>' ).text( 'Demo styles failed to load.' ) );
279                 } );
280 };
281
282 /**
283  * Handle mode change events.
284  *
285  * Will load a new page.
286  */
287 Demo.prototype.onModeChange = function () {
288         var page = this.pageMenu.getSelectedItem().getData(),
289                 theme = this.themeSelect.getSelectedItem().getData(),
290                 direction = this.directionSelect.getSelectedItem().getData(),
291                 platform = this.platformSelect.getSelectedItem().getData();
292
293         history.pushState( null, document.title, this.getUrlQuery( [ page, theme, direction, platform ] ) );
294         $( window ).triggerHandler( 'popstate' );
295 };
296
297 /**
298  * Get URL query for given factors describing the demo's mode.
299  *
300  * @param {string[]} factors Factors, as returned e.g. by #getCurrentFactorValues
301  * @return {string} URL query part, starting with '?'
302  */
303 Demo.prototype.getUrlQuery = function ( factors ) {
304         return '?page=' + factors[ 0 ] +
305                 '&theme=' + factors[ 1 ] +
306                 '&direction=' + factors[ 2 ] +
307                 '&platform=' + factors[ 3 ] +
308                 // Preserve current URL 'fragment' part
309                 location.hash;
310 };
311
312 /**
313  * Get a list of mode factors.
314  *
315  * Factors are a mapping between symbolic names used in the URL query and internal information used
316  * to act on those symbolic names.
317  *
318  * Factor lists are in URL order: page, theme, direction, platform. Page contains the symbolic
319  * page name, others contain file suffixes.
320  *
321  * @return {Object[]} List of mode factors, keyed by symbolic name
322  */
323 Demo.prototype.getFactors = function () {
324         var key,
325                 factors = [ {}, {}, {}, {} ];
326
327         for ( key in this.constructor.static.pages ) {
328                 factors[ 0 ][ key ] = key;
329         }
330         for ( key in this.constructor.static.themes ) {
331                 factors[ 1 ][ key ] = '-' + key;
332         }
333         for ( key in this.constructor.static.directions ) {
334                 factors[ 2 ][ key ] = this.constructor.static.directions[ key ].fileSuffix;
335         }
336         this.constructor.static.platforms.forEach( function ( platform ) {
337                 factors[ 3 ][ platform ] = '';
338         } );
339
340         return factors;
341 };
342
343 /**
344  * Get a list of default factors.
345  *
346  * Factor defaults are in URL order: page, theme, direction, platform. Each contains a symbolic
347  * factor name which should be used as a fallback when the URL query is missing or invalid.
348  *
349  * @return {Object[]} List of default factors
350  */
351 Demo.prototype.getDefaultFactorValues = function () {
352         return [
353                 this.constructor.static.defaultPage,
354                 this.constructor.static.defaultTheme,
355                 this.constructor.static.defaultDirection,
356                 this.constructor.static.defaultPlatform
357         ];
358 };
359
360 /**
361  * Parse the current URL query into factor values.
362  *
363  * @return {string[]} Factor values in URL order: page, theme, direction, platform
364  */
365 Demo.prototype.getCurrentFactorValues = function () {
366         var i, parts, index,
367                 factors = this.getDefaultFactorValues(),
368                 order = [ 'page', 'theme', 'direction', 'platform' ],
369                 query = location.search.slice( 1 ).split( '&' );
370         for ( i = 0; i < query.length; i++ ) {
371                 parts = query[ i ].split( '=', 2 );
372                 index = order.indexOf( parts[ 0 ] );
373                 if ( index !== -1 ) {
374                         factors[ index ] = decodeURIComponent( parts[ 1 ] );
375                 }
376         }
377         return factors;
378 };
379
380 /**
381  * Get the current mode.
382  *
383  * Generated from parsed URL query values.
384  *
385  * @return {Object} List of factor values keyed by factor name
386  */
387 Demo.prototype.getCurrentMode = function () {
388         var factorValues = this.getCurrentFactorValues();
389
390         return {
391                 page: factorValues[ 0 ],
392                 theme: factorValues[ 1 ],
393                 direction: factorValues[ 2 ],
394                 platform: factorValues[ 3 ]
395         };
396 };
397
398 /**
399  * Get link elements for the current mode.
400  *
401  * @return {HTMLElement[]} List of link elements
402  */
403 Demo.prototype.getStylesheetLinks = function () {
404         var i, len, links, fragments,
405                 factors = this.getFactors(),
406                 theme = this.getCurrentFactorValues()[ 1 ],
407                 suffixes = this.constructor.static.additionalThemeImagesSuffixes[ theme ] || [],
408                 urls = [];
409
410         // Translate modes to filename fragments
411         fragments = this.getCurrentFactorValues().map( function ( val, index ) {
412                 return factors[ index ][ val ];
413         } );
414
415         // Theme styles
416         urls.push( 'dist/oojs-ui' + fragments.slice( 1 ).join( '' ) + '.css' );
417         for ( i = 0, len = suffixes.length; i < len; i++ ) {
418                 urls.push( 'dist/oojs-ui' + fragments[ 1 ] + suffixes[ i ] + fragments[ 2 ] + '.css' );
419         }
420
421         // Demo styles
422         urls.push( 'styles/demo' + fragments[ 2 ] + '.css' );
423
424         // Add link tags
425         links = urls.map( function ( url ) {
426                 var
427                         link = document.createElement( 'link' ),
428                         $link = $( link ),
429                         deferred = $.Deferred();
430                 $link.data( 'load-promise', deferred.promise() );
431                 $link.on( {
432                         load: deferred.resolve,
433                         error: deferred.reject
434                 } );
435                 link.rel = 'stylesheet';
436                 link.href = url;
437                 return link;
438         } );
439
440         return links;
441 };
442
443 /**
444  * Normalize the URL query.
445  */
446 Demo.prototype.normalizeQuery = function () {
447         var i, len, factorValues, match, valid, factorValue,
448                 modes = [],
449                 factors = this.getFactors(),
450                 defaults = this.getDefaultFactorValues();
451
452         factorValues = this.getCurrentFactorValues();
453         for ( i = 0, len = factors.length; i < len; i++ ) {
454                 factorValue = factorValues[ i ];
455                 modes[ i ] = factors[ i ][ factorValue ] !== undefined ? factorValue : defaults[ i ];
456         }
457
458         // Backwards-compatibility with old URLs that used the 'fragment' part to link to demo sections:
459         // if a fragment is specified and it describes valid factors, turn the URL into the new style.
460         match = location.hash.match( /^#(\w+)-(\w+)-(\w+)-(\w+)$/ );
461         if ( match ) {
462                 factorValues = [];
463                 valid = true;
464                 for ( i = 0, len = factors.length; i < len; i++ ) {
465                         factorValue = match[ i + 1 ];
466                         if ( factors[ i ][ factorValue ] !== undefined ) {
467                                 factorValues[ i ] = factorValue;
468                         } else {
469                                 valid = false;
470                                 break;
471                         }
472                 }
473                 if ( valid ) {
474                         location.hash = '';
475                         modes = factorValues;
476                 }
477         }
478
479         // Update query
480         history.replaceState( null, document.title, this.getUrlQuery( modes ) );
481 };
482
483 /**
484  * Destroy demo.
485  */
486 Demo.prototype.destroy = function () {
487         $( 'body' ).removeClass( 'oo-ui-ltr oo-ui-rtl' );
488         $( this.stylesheetLinks ).remove();
489         this.$element.remove();
490         this.emit( 'destroy' );
491 };
492
493 /**
494  * Build a console for interacting with an element.
495  *
496  * @param {OO.ui.Layout} item
497  * @param {string} layout Variable name for layout
498  * @param {string} widget Variable name for layout's field widget
499  * @return {jQuery} Console interface element
500  */
501 Demo.prototype.buildConsole = function ( item, layout, widget, showLayoutCode ) {
502         var $toggle, $log, $label, $input, $submit, $console, $form, $pre, $code,
503                 console = window.console;
504
505         function exec( str ) {
506                 var func, ret;
507                 if ( str.indexOf( 'return' ) !== 0 ) {
508                         str = 'return ' + str;
509                 }
510                 try {
511                         // eslint-disable-next-line no-new-func
512                         func = new Function( layout, widget, 'item', str );
513                         ret = { value: func( item, item.fieldWidget, item.fieldWidget ) };
514                 } catch ( error ) {
515                         ret = {
516                                 value: undefined,
517                                 error: error
518                         };
519                 }
520                 return ret;
521         }
522
523         function submit() {
524                 var val, result, logval;
525
526                 val = $input.val();
527                 $input.val( '' );
528                 $input[ 0 ].focus();
529                 result = exec( val );
530
531                 logval = String( result.value );
532                 if ( logval === '' ) {
533                         logval = '""';
534                 }
535
536                 $log.append(
537                         $( '<div>' )
538                                 .addClass( 'demo-console-log-line demo-console-log-line-input' )
539                                 .text( val ),
540                         $( '<div>' )
541                                 .addClass( 'demo-console-log-line demo-console-log-line-return' )
542                                 .text( logval || result.value )
543                 );
544
545                 if ( result.error ) {
546                         $log.append( $( '<div>' ).addClass( 'demo-console-log-line demo-console-log-line-error' ).text( result.error ) );
547                 }
548
549                 if ( console && console.log ) {
550                         console.log( '[demo]', result.value );
551                         if ( result.error ) {
552                                 if ( console.error ) {
553                                         console.error( '[demo]', String( result.error ), result.error );
554                                 } else {
555                                         console.log( '[demo] Error: ', result.error );
556                                 }
557                         }
558                 }
559
560                 // Scrol to end
561                 $log.prop( 'scrollTop', $log.prop( 'scrollHeight' ) );
562         }
563
564         function getCode( item, toplevel ) {
565                 var config, defaultConfig, url, params, out, i,
566                         items = [],
567                         demoLinks = [],
568                         docLinks = [];
569
570                 function getConstructorName( item ) {
571                         var isDemoWidget = item.constructor.name.indexOf( 'Demo' ) === 0;
572                         return ( isDemoWidget ? 'Demo.' : 'OO.ui.' ) + item.constructor.name.slice( 4 );
573                 }
574
575                 // If no item was passed we shouldn't show a code block
576                 if ( item === undefined ) {
577                         return false;
578                 }
579
580                 config = item.initialConfig;
581
582                 // Prevent the default config from being part of the code
583                 if ( item instanceof OO.ui.ActionFieldLayout ) {
584                         defaultConfig = ( new item.constructor( new OO.ui.TextInputWidget(), new OO.ui.ButtonWidget() ) ).initialConfig;
585                 } else if ( item instanceof OO.ui.FieldLayout ) {
586                         defaultConfig = ( new item.constructor( new OO.ui.ButtonWidget() ) ).initialConfig;
587                 } else {
588                         defaultConfig = ( new item.constructor() ).initialConfig;
589                 }
590                 Object.keys( defaultConfig ).forEach( function ( key ) {
591                         if ( config[ key ] === defaultConfig[ key ] ) {
592                                 delete config[ key ];
593                         } else if (
594                                 typeof config[ key ] === 'object' && typeof defaultConfig[ key ] === 'object' &&
595                                 OO.compare( config[ key ], defaultConfig[ key ] )
596                         ) {
597                                 delete config[ key ];
598                         }
599                 } );
600
601                 config = javascriptStringify( config, function ( obj, indent, stringify ) {
602                         if ( obj instanceof Function ) {
603                                 // Get function's source code, with extraneous indentation removed
604                                 return obj.toString().replace( /^\t\t\t\t\t\t/gm, '' );
605                         } else if ( obj instanceof jQuery ) {
606                                 if ( $.contains( item.$element[ 0 ], obj[ 0 ] ) ) {
607                                         // If this element appears inside the generated widget,
608                                         // assume this was something like `$label: $( '<p>Text</p>' )`
609                                         return '$( ' + javascriptStringify( obj.prop( 'outerHTML' ) ) + ' )';
610                                 } else {
611                                         // Otherwise assume this was something like `$overlay: $( '#overlay' )`
612                                         return '$( ' + javascriptStringify( '#' + obj.attr( 'id' ) ) + ' )';
613                                 }
614                         } else if ( obj instanceof OO.ui.HtmlSnippet ) {
615                                 return 'new OO.ui.HtmlSnippet( ' + javascriptStringify( obj.toString() ) + ' )';
616                         } else if ( obj instanceof OO.ui.Element ) {
617                                 return getCode( obj );
618                         } else {
619                                 return stringify( obj );
620                         }
621                 }, '\t' );
622
623                 // The generated code needs to include different arguments, based on the object type
624                 items.push( item );
625                 if ( item instanceof OO.ui.ActionFieldLayout ) {
626                         params = getCode( item.fieldWidget ) + ', ' + getCode( item.buttonWidget );
627                         items.push( item.fieldWidget );
628                         items.push( item.buttonWidget );
629                 } else if ( item instanceof OO.ui.FieldLayout ) {
630                         params = getCode( item.fieldWidget );
631                         items.push( item.fieldWidget );
632                 } else {
633                         params = '';
634                 }
635                 if ( config !== '{}' ) {
636                         params += ( params ? ', ' : '' ) + config;
637                 }
638                 out = 'new ' + getConstructorName( item ) + '(' +
639                         ( params ? ' ' : '' ) + params + ( params ? ' ' : '' ) +
640                         ')';
641
642                 if ( toplevel ) {
643                         for ( i = 0; i < items.length; i++ ) {
644                                 item = items[ i ];
645                                 // The code generated for Demo widgets cannot be copied and used
646                                 if ( item.constructor.name.indexOf( 'Demo' ) === 0 ) {
647                                         url =
648                                                 'https://phabricator.wikimedia.org/diffusion/GOJU/browse/master/demos/classes/' +
649                                                 item.constructor.name.slice( 4 ) + '.js';
650                                         demoLinks.push( url );
651                                 } else {
652                                         url = 'https://doc.wikimedia.org/oojs-ui/master/js/#!/api/' + getConstructorName( item );
653                                         url = '[' + url + '](' + url + ')';
654                                         docLinks.push( url );
655                                 }
656                         }
657                 }
658
659                 return (
660                         ( docLinks.length ? '// See documentation at: \n// ' : '' ) +
661                         docLinks.join( '\n// ' ) + ( docLinks.length ? '\n' : '' ) +
662                         ( demoLinks.length ? '// See source code:\n// ' : '' ) +
663                         demoLinks.join( '\n// ' ) + ( demoLinks.length ? '\n' : '' ) +
664                         out
665                 );
666         }
667
668         $toggle = $( '<span>' )
669                 .addClass( 'demo-console-toggle' )
670                 .attr( 'title', 'Toggle console' )
671                 .on( 'click', function ( e ) {
672                         var code;
673                         e.preventDefault();
674                         $console.toggleClass( 'demo-console-collapsed demo-console-expanded' );
675                         if ( $input.is( ':visible' ) ) {
676                                 $input[ 0 ].focus();
677                                 if ( console && console.log ) {
678                                         window[ layout ] = item;
679                                         window[ widget ] = item.fieldWidget;
680                                         console.log( '[demo]', 'Globals ' + layout + ', ' + widget + ' have been set' );
681                                         console.log( '[demo]', item );
682
683                                         if ( showLayoutCode === true ) {
684                                                 code = getCode( item, true );
685                                         } else {
686                                                 code = getCode( item.fieldWidget, true );
687                                         }
688
689                                         if ( code ) {
690                                                 $code.text( code );
691                                                 Prism.highlightElement( $code[ 0 ] );
692                                         } else {
693                                                 $code.remove();
694                                         }
695                                 }
696                         }
697                 } );
698
699         $log = $( '<div>' )
700                 .addClass( 'demo-console-log' );
701
702         $label = $( '<label>' )
703                 .addClass( 'demo-console-label' );
704
705         $input = $( '<input>' )
706                 .addClass( 'demo-console-input' )
707                 .prop( 'placeholder', '... (predefined: ' + layout + ', ' + widget + ')' );
708
709         $submit = $( '<div>' )
710                 .addClass( 'demo-console-submit' )
711                 .text( '↵' )
712                 .on( 'click', submit );
713
714         $form = $( '<form>' ).on( 'submit', function ( e ) {
715                 e.preventDefault();
716                 submit();
717         } );
718
719         $code = $( '<code>' ).addClass( 'language-javascript' );
720
721         $pre = $( '<pre>' )
722                 .addClass( 'demo-sample-code' )
723                 .append( $code );
724
725         $console = $( '<div>' )
726                 .addClass( 'demo-console demo-console-collapsed' )
727                 .append(
728                         $toggle,
729                         $log,
730                         $form.append(
731                                 $label.append(
732                                         $input
733                                 ),
734                                 $submit
735                         ),
736                         $pre
737                 );
738
739         return $console;
740 };
741
742 /**
743  * Build a link to this example.
744  *
745  * @param {OO.ui.Layout} item
746  * @param {OO.ui.FieldsetLayout} parentItem
747  * @return {jQuery} Link interface element
748  */
749 Demo.prototype.buildLinkExample = function ( item, parentItem ) {
750         var $linkExample, label, fragment;
751
752         if ( item.$label.text() === '' ) {
753                 item = parentItem;
754         }
755         fragment = item.elementId;
756         if ( !fragment ) {
757                 label = item.$label.text();
758                 fragment = label.replace( /[^\w]+/g, '-' ).replace( /^-|-$/g, '' );
759                 item.setElementId( fragment );
760         }
761
762         $linkExample = $( '<a>' )
763                 .addClass( 'demo-link-example' )
764                 .attr( 'title', 'Link to this example' )
765                 .attr( 'href', '#' + fragment )
766                 .on( 'click', function ( e ) {
767                         // We have to handle this manually in order to call .scrollToFragment() even if it's the same
768                         // fragment. Normally, the browser will scroll but not fire a 'hashchange' event in this
769                         // situation, and the scroll position will be off because of our fixed header.
770                         if ( e.which === OO.ui.MouseButtons.LEFT ) {
771                                 location.hash = $( this ).attr( 'href' );
772                                 Demo.static.scrollToFragment();
773                                 e.preventDefault();
774                         }
775                 } );
776
777         return $linkExample;
778 };