%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/Translate/resources/js/ |
Current File : /www/varak.net/wiki.varak.net/extensions/Translate/resources/js/ext.translate.messagetable.js |
( function ( $, mw ) { 'use strict'; var itemsClass = { proofread: '.tux-message-proofread', page: '.tux-message-pagemode', translate: '.tux-message' }; mw.translate = mw.translate || {}; mw.translate = $.extend( mw.translate, { getMessages: function ( messageGroup, language, offset, limit, filter ) { var api = new mw.Api(); return api.get( { action: 'query', list: 'messagecollection', mcgroup: messageGroup, mclanguage: language, mcoffset: offset, mclimit: limit, mcfilter: filter, mcprop: 'definition|translation|tags|properties', rawcontinue: 1, errorformat: 'html' } ); } } ); function MessageTable( container, options, settings ) { this.$container = $( container ); this.options = options; this.options = $.extend( {}, $.fn.messagetable.defaults, options ); this.settings = settings; // mode can be proofread, page or translate this.mode = this.options.mode; this.firstProofreadTipShown = false; this.initialized = false; this.$header = this.$container.siblings( '.tux-messagetable-header' ); // Container is between these in the dom. this.$loader = this.$container.siblings( '.tux-messagetable-loader' ); this.$loaderIcon = this.$loader.find( '.tux-loading-indicator' ); this.$loaderInfo = this.$loader.find( '.tux-messagetable-loader-info' ); this.$actionBar = this.$container.siblings( '.tux-action-bar' ); this.$statsBar = this.$actionBar.find( '.tux-message-list-statsbar' ); this.$proofreadOwnTranslations = this.$actionBar.find( '.tux-proofread-own-translations-button' ); this.messages = []; this.loading = false; this.init(); this.listen(); } MessageTable.prototype = { init: function () { this.$actionBar.removeClass( 'hide' ); }, listen: function () { var messageTable = this, $filterInput = this.$container.parent().find( '.tux-message-filter-box' ); // Vector has transitions of 250ms which affect layout. Let those finish. $( window ).on( 'scroll', $.debounce( 250, function () { messageTable.scroll(); if ( isLoaderVisible( messageTable.$loader ) ) { messageTable.load(); } } ) ).on( 'resize', $.throttle( 250, function () { messageTable.resize(); messageTable.scroll(); } ) ); if ( mw.translate.isPlaceholderSupported( $filterInput ) ) { $filterInput.prop( 'placeholder', mw.msg( 'tux-message-filter-placeholder' ) ); } $filterInput.on( 'textchange', $.debounce( 250, function () { messageTable.search( $filterInput.val() ); } ) ); this.$actionBar.find( 'button.proofread-mode-button' ).on( 'click', function () { messageTable.switchMode( 'proofread' ); } ); this.$actionBar.find( 'button.translate-mode-button' ).on( 'click', function () { messageTable.switchMode( 'translate' ); } ); this.$actionBar.find( 'button.page-mode-button' ).on( 'click', function () { messageTable.switchMode( 'page' ); } ); this.$proofreadOwnTranslations.click( function () { var $this = $( this ), hideMessage = mw.msg( 'tux-editor-proofreading-hide-own-translations' ), showMessage = mw.msg( 'tux-editor-proofreading-show-own-translations' ); if ( $this.hasClass( 'down' ) ) { messageTable.setHideOwnInProofreading( false ); $this.removeClass( 'down' ).text( hideMessage ); } else { messageTable.setHideOwnInProofreading( true ); $this.addClass( 'down' ).text( showMessage ); } } ); }, /** * Clear the message table */ clear: function () { this.$container.empty(); $( '.translate-tooltip' ).remove(); this.messages = []; // Any ongoing loading process will notice this and will reject results. this.loading = false; }, /** * Adds a new message using current mode. * * @param {Object} message */ add: function ( message ) { // Prepare the message for display mw.translateHooks.run( 'formatMessageBeforeTable', message ); if ( this.mode === 'translate' ) { this.addTranslate( message ); } else if ( this.mode === 'proofread' ) { this.addProofread( message ); } else if ( this.mode === 'page' ) { this.addPageModeMessage( message ); } }, /** * Add a message to the message table for translation. * * @param {Object} message */ addTranslate: function ( message ) { var $message, targetLangDir, targetLangAttrib, targetLangCode = this.$container.data( 'targetlangcode' ), sourceLangCode = this.$container.data( 'sourcelangcode' ), sourceLangDir = $.uls.data.getDir( sourceLangCode ), status = message.properties.status, statusClass = 'tux-status-' + status, $messageWrapper = $( '<div>' ).addClass( 'row tux-message' ), statusMsg = ''; if ( message.tags.length && message.tags.indexOf( 'optional' ) >= 0 && status === 'untranslated' ) { status = 'optional'; statusClass = 'tux-status-optional'; } // Fuzzy translations need warning class if ( status === 'fuzzy' ) { statusClass = statusClass + ' tux-warning'; } // Label the status if it is not untranslated if ( status !== 'untranslated' ) { // Give grep a chance to find the usages: // tux-status-optional, tux-status-fuzzy, tux-status-proofread, // tux-status-translated, tux-status-saving, tux-status-unsaved statusMsg = 'tux-status-' + status; } if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { targetLangAttrib = mw.config.get( 'wgContentLanguage' ); targetLangDir = $.uls.data.getDir( targetLangAttrib ); } else { targetLangAttrib = targetLangCode; targetLangDir = this.$container.data( 'targetlangdir' ); } $message = $( '<div>' ) .addClass( 'row message tux-message-item ' + status ) .append( $( '<div>' ) .addClass( 'eight columns tux-list-message' ) .append( $( '<span>' ) .addClass( 'tux-list-source' ) .attr( { lang: sourceLangCode, dir: sourceLangDir } ) .text( message.definition ), // Bidirectional isolation. // This should be removed some day when proper // unicode-bidi: isolate // is supported everywhere $( '<span>' ) .html( $( 'body' ).hasClass( 'rtl' ) ? '‏' : '‎' ), $( '<span>' ) .addClass( 'tux-list-translation' ) .attr( { lang: targetLangAttrib, dir: targetLangDir } ) .text( message.translation || '' ) ), $( '<div>' ) .addClass( 'two columns tux-list-status text-center' ) .append( $( '<span>' ) .addClass( statusClass ) .text( statusMsg ? mw.msg( statusMsg ) : '' ) ), $( '<div>' ) .addClass( 'two column tux-list-edit text-right' ) .append( $( '<a>' ) .attr( { title: mw.msg( 'translate-edit-title', message.key ), href: mw.util.getUrl( message.title, { action: 'edit' } ) } ) .text( mw.msg( 'tux-edit' ) ) ) ); $messageWrapper.append( $message ); this.$container.append( $messageWrapper ); // Attach translate editor to the message $messageWrapper.translateeditor( { message: message } ); }, /** * Add a message to the message table for proofreading. * * @param {Object} message */ addProofread: function ( message ) { var $message, $icon; $message = $( '<div>' ).addClass( 'row tux-message-proofread' ); this.$container.append( $message ); $message.proofread( { message: message, sourcelangcode: this.$container.data( 'sourcelangcode' ), targetlangcode: this.$container.data( 'targetlangcode' ) } ); $icon = $message.find( '.tux-proofread-action' ); if ( $icon.length === 0 ) { return; } // Add autotooltip to first available proofread action icon if ( this.firstProofreadTipShown ) { return; } this.firstProofreadTipShown = true; $icon.addClass( 'autotooltip' ); mw.loader.using( 'oojs-ui-core' ).done( function () { var tooltip = new OO.ui.PopupWidget( { padded: true, align: 'center', width: 250, classes: [ 'translate-tooltip' ], $content: $( '<p>' ).text( $icon.prop( 'title' ) ) } ); setTimeout( function () { var offset, $icon = $( '.autotooltip:visible' ); if ( !$icon.length ) { return; } offset = $icon.offset(); tooltip.$element.appendTo( 'body' ); tooltip.toggle( true ).toggleClipping( false ).togglePositioning( false ); tooltip.$element.css( { top: offset.top + $icon.outerHeight() + 5, left: offset.left + $icon.outerWidth() - tooltip.$element.width() / 2 - 15 } ); setTimeout( function () { tooltip.$element.remove(); }, 4000 ); }, 1000 ); } ); }, /** * Add a message to the message table for wiki page mode. * * @param {Object} message */ addPageModeMessage: function ( message ) { var $message; $message = $( '<div>' ) .addClass( 'row tux-message tux-message-pagemode' ); this.$container.append( $message ); $message.pagemode( { message: message, sourcelangcode: this.$container.data( 'sourcelangcode' ), targetlangcode: this.$container.data( 'targetlangcode' ) } ); }, /** * Search the message filter * * @param {string} query */ search: function ( query ) { var $note, $button, $result, resultCount = 0, matcher = new RegExp( '(^|\\s|\\b)' + escapeRegex( query ), 'gi' ); this.$container.find( itemsClass[ this.mode ] ).each( function () { var $message = $( this ), message = ( $message.data( 'translateeditor' ) || $message.data( 'pagemode' ) || $message.data( 'proofread' ) ).message; if ( matcher.test( message.definition ) || matcher.test( message.translation ) ) { $message.removeClass( 'hide' ); resultCount++; } else { $message.addClass( 'hide' ); } } ); $result = this.$container.find( '.tux-message-filter-result' ); if ( !$result.length ) { $note = $( '<div>' ) .addClass( 'advanced-search' ); $button = $( '<button>' ) .addClass( 'mw-ui-button' ) .text( mw.msg( 'tux-message-filter-advanced-button' ) ); $result = $( '<div>' ) .addClass( 'tux-message-filter-result' ) .append( $note, $button ); this.$container.prepend( $result ); } if ( !query ) { $result.addClass( 'hide' ); } else { $result.removeClass( 'hide' ) .find( '.advanced-search' ) .text( mw.msg( 'tux-message-filter-result', resultCount, query ) ); $result.find( 'button' ).on( 'click', function () { window.location.href = mw.util.getUrl( 'Special:SearchTranslations', { query: query } ); } ); } this.updateLastMessage(); // Trigger a scroll event for the window to make sure all floating toolbars // are in their position. $( window ).trigger( 'scroll' ); }, resize: function () { var actualWidth = 0; // Calculate the total width required for the filters $( '.row.tux-message-selector > li' ).each( function () { actualWidth += $( this ).outerWidth( true ); } ); // Grid row has a min width. After that scrollbars will appear. // We are checking whether the message table is wider than the current grid row width. if ( actualWidth >= parseInt( $( '.nine.columns' ).width(), 10 ) ) { $( '.tux-message-selector .more ul' ) // Overflow menu .prepend( $( '.row.tux-message-selector > li.column:last' ).prev() ); // See if more items to be pushed to the overflow menu this.resize(); } }, /** * Start loading messages again with new settings. * * @param {Object} changes */ changeSettings: function ( changes ) { // Clear current messages this.clear(); this.settings = $.extend( this.settings, changes ); if ( this.initialized === false ) { this.switchMode( this.mode ); } // Reset the number of messages remaining this.$loaderInfo.text( mw.msg( 'tux-messagetable-loading-messages', this.$loader.data( 'pagesize' ) ) ); // Reset the statsbar this.$statsBar .empty() .removeData() .languagestatsbar( { language: this.settings.language, group: this.settings.group } ); this.initialized = true; // Reset other info and make visible this.$loader .removeData( 'offset' ) .removeAttr( 'data-offset' ) .removeClass( 'hide' ); if ( changes.offset ) { this.$loader.data( 'offset', changes.offset ); } this.$header.removeClass( 'hide' ); this.$actionBar.removeClass( 'hide' ); // Start loading messages this.load( changes.limit ); }, /** * @param {number} [limit] Only load this many messages and then stop even if there is more. */ load: function ( limit ) { var remaining, query, self = this, offset = this.$loader.data( 'offset' ), pageSize = limit || this.$loader.data( 'pagesize' ); if ( offset === -1 ) { return; } if ( this.loading ) { // Avoid duplicate loading - the offset will be wrong and it will result // in duplicate messages shown in the page return; } this.loading = true; this.$loaderIcon.removeClass( 'tux-loading-indicator--stopped' ); mw.translate.getMessages( this.settings.group, this.settings.language, offset, pageSize, this.settings.filter ).done( function ( result ) { var messages = result.query.messagecollection, state; if ( !self.loading ) { // reject. This was cancelled. return; } if ( messages.length === 0 ) { // And this is the first load for the filter... if ( self.$container.children().length === 0 ) { self.displayEmptyListHelp(); } } $.each( messages, function ( index, message ) { message.group = self.settings.group; self.add( message ); self.messages.push( message ); if ( index === 0 && self.mode === 'translate' ) { $( '.tux-message:first' ).data( 'translateeditor' ).init(); } } ); state = result.query.metadata && result.query.metadata.state; $( '.tux-workflow' ).workflowselector( self.settings.group, self.settings.language, state ); // Dynamically loaded messages should pass the search filter if present. query = $( '.tux-message-filter-box' ).val(); if ( query ) { self.search( query ); } if ( result[ 'query-continue' ] === undefined || limit ) { // End of messages self.$loader.data( 'offset', -1 ) .addClass( 'hide' ); // Helpfully open the first message in show mode // TODO: Refactor to avoid direct DOM access $( '.tux-message-item' ).first().click(); } else { self.$loader.data( 'offset', result[ 'query-continue' ].messagecollection.mcoffset ); remaining = result.query.metadata.remaining; self.$loaderInfo.text( mw.msg( 'tux-messagetable-more-messages', remaining ) ); // Make sure the floating toolbars are visible without the need for scroll $( window ).trigger( 'scroll' ); } self.updateHideOwnInProofreadingToggleVisibility(); self.updateLastMessage(); } ).fail( function ( errorCode, response ) { var $warningContainer = $( '.tux-editor-header .group-warning' ); if ( response.errors ) { $.map( response.errors, function ( error ) { $warningContainer.append( error[ '*' ] ); } ); } else { $warningContainer.text( mw.msg( 'api-error-unknownerror', errorCode ) ); } self.$loader.data( 'offset', -1 ).addClass( 'hide' ); self.$actionBar.addClass( 'hide' ); self.$header.addClass( 'hide' ); } ).always( function () { self.$loaderIcon.addClass( 'tux-loading-indicator--stopped' ); self.loading = false; } ); }, updateLastMessage: function () { var $messages = this.$container.find( itemsClass[ this.mode ] ); // If a message was previously marked as "last", restore it to normal state $messages.filter( '.last-message' ).removeClass( 'last-message' ); // At the class to the current last shown message $messages .not( '.hide' ) .last() .addClass( 'last-message' ); }, /** * Creates a uniformly styled button for different actions, * shown when there are no messages to display. * * @param {string} labelMsg A message key for the button label. * @param {Function} callback A callback for clicking the button. * @return {jQuery} A button element. */ otherActionButton: function ( labelMsg, callback ) { return $( '<button>' ) .addClass( 'mw-ui-button mw-ui-progressive mw-ui-big' ) .text( mw.msg( labelMsg ) ) .on( 'click', callback ); }, /** * Enables own message hiding in proofread mode. * * @param {boolean} enabled */ setHideOwnInProofreading: function ( enabled ) { if ( enabled ) { this.$container.addClass( 'tux-hide-own' ); } else { this.$container.removeClass( 'tux-hide-own' ); } }, updateHideOwnInProofreadingToggleVisibility: function () { if ( this.$container.find( '.tux-message-proofread.own-translation' ).length ) { this.$proofreadOwnTranslations.removeClass( 'hide' ); } else { this.$proofreadOwnTranslations.addClass( 'hide' ); } }, /** * If the user selection doesn't show anything, * give some pointers to other things to do. */ displayEmptyListHelp: function () { var messageTable = this, // @todo Ugly! This should be provided somehow selectedTab = $( '.tux-message-selector .selected' ).data( 'title' ), $wrap = $( '<div>' ).addClass( 'tux-empty-list' ), $emptyListHeader = $( '<div>' ).addClass( 'tux-empty-list-header' ), $guide = $( '<div>' ).addClass( 'tux-empty-list-guide' ), $actions = $( '<div>' ).addClass( 'tux-empty-list-actions' ); if ( messageTable.mode === 'proofread' ) { if ( selectedTab === 'all' ) { $emptyListHeader.text( mw.msg( 'tux-empty-no-messages-to-display' ) ); $guide.append( $( '<p>' ) .text( mw.msg( 'tux-empty-there-are-optional' ) ), $( '<a>' ) .attr( 'href', '#' ) .text( mw.msg( 'tux-empty-show-optional-messages' ) ) .on( 'click', function ( e ) { $( '#tux-option-optional' ).click(); e.preventDefault(); } ) ); } else if ( selectedTab === 'outdated' ) { $emptyListHeader.text( mw.msg( 'tux-empty-no-outdated-messages' ) ); $guide.text( mw.msg( 'tux-empty-list-other-guide' ) ); $actions.append( messageTable.otherActionButton( 'tux-empty-list-other-action', function () { $( '.tux-tab-unproofread' ).click(); // @todo untranslated } ) ); // @todo View all } else if ( selectedTab === 'translated' ) { $emptyListHeader.text( mw.msg( 'tux-empty-nothing-to-proofread' ) ); $guide.text( mw.msg( 'tux-empty-you-can-help-providing' ) ); $actions.append( messageTable.otherActionButton( 'tux-empty-list-translated-action', function () { messageTable.switchMode( 'translate' ); } ) ); } else if ( selectedTab === 'unproofread' ) { $emptyListHeader.text( mw.msg( 'tux-empty-nothing-new-to-proofread' ) ); $guide.text( mw.msg( 'tux-empty-you-can-help-providing' ) ); $actions.append( messageTable.otherActionButton( 'tux-empty-you-can-review-already-proofread', function () { $( '.tux-tab-translated' ).click(); } ) ); } } else { if ( selectedTab === 'all' ) { $emptyListHeader.text( mw.msg( 'tux-empty-list-all' ) ); $guide.text( mw.msg( 'tux-empty-list-all-guide' ) ); } else if ( selectedTab === 'translated' ) { $emptyListHeader.text( mw.msg( 'tux-empty-list-translated' ) ); $guide.text( mw.msg( 'tux-empty-list-translated-guide' ) ); $actions.append( messageTable.otherActionButton( 'tux-empty-list-translated-action', function () { mw.translate.changeFilter( $( '.tux-tab-untranslated' ).click() ); } ) ); } else { $emptyListHeader.text( mw.msg( 'tux-empty-list-other' ) ); if ( mw.translate.canProofread() ) { $guide.text( mw.msg( 'tux-empty-list-other-guide' ) ); $actions.append( messageTable.otherActionButton( 'tux-empty-list-other-action', function () { messageTable.switchMode( 'proofread' ); } ) ); } $actions.append( $( '<a>' ) .text( mw.msg( 'tux-empty-list-other-link' ) ) .click( function () { $( '.tux-tab-all' ).click(); } ) ); } } $wrap.append( $emptyListHeader, $guide, $actions ); this.$container.append( $wrap ); }, /** * Switch the message table mode * * @param {string} mode The message table mode to switch to: translate, page or proofread */ switchMode: function ( mode ) { var messageTable = this, filter = this.settings.filter, userId = mw.config.get( 'wgUserId' ), $tuxTabUntranslated, $tuxTabUnproofread, $hideTranslatedButton; messageTable.$actionBar.find( '.tux-view-switcher .down' ).removeClass( 'down' ); if ( mode === 'translate' ) { messageTable.$actionBar.find( '.translate-mode-button' ).addClass( 'down' ); } if ( mode === 'proofread' ) { messageTable.$actionBar.find( '.proofread-mode-button' ).addClass( 'down' ); } if ( mode === 'page' ) { messageTable.$actionBar.find( '.page-mode-button' ).addClass( 'down' ); } messageTable.firstProofreadTipShown = false; messageTable.mode = mode; mw.translate.changeUrl( { action: messageTable.mode } ); // Emulate clear without clearing loaded messages messageTable.$container.empty(); $( '.translate-tooltip' ).remove(); $tuxTabUntranslated = $( '.tux-message-selector > .tux-tab-untranslated' ); $tuxTabUnproofread = $( '.tux-message-selector > .tux-tab-unproofread' ); $hideTranslatedButton = messageTable.$actionBar.find( '.tux-editor-clear-translated' ); if ( messageTable.mode === 'proofread' ) { $tuxTabUntranslated.addClass( 'hide' ); $tuxTabUnproofread.removeClass( 'hide' ); // Fix the filter if it is untranslated. Untranslated does not make sense // for proofread mode. Keep the filter if it is not 'untranslated' if ( !filter || filter.indexOf( '!translated' ) >= 0 ) { messageTable.messages = []; // default filter for proofread mode mw.translate.changeFilter( 'translated|!reviewer:' + userId + '|!last-translator:' + userId ); $tuxTabUnproofread.addClass( 'selected' ); // Own translations are not present in proofread + unreviewed mode } $hideTranslatedButton.addClass( 'hide' ); } else { $tuxTabUntranslated.removeClass( 'hide' ); $tuxTabUnproofread.addClass( 'hide' ); if ( filter.indexOf( '!translated' ) > -1 ) { $hideTranslatedButton.removeClass( 'hide' ); } if ( filter && filter.indexOf( '!last-translator' ) >= 0 ) { messageTable.messages = []; // default filter for translate mode mw.translate.changeFilter( '!translated' ); $tuxTabUntranslated.addClass( 'selected' ); } } if ( messageTable.messages.length ) { $.each( messageTable.messages, function ( index, message ) { messageTable.add( message ); } ); } else if ( messageTable.initialized ) { messageTable.displayEmptyListHelp(); } this.$loaderInfo.text( mw.msg( 'tux-messagetable-loading-messages', this.$loader.data( 'pagesize' ) ) ); messageTable.updateHideOwnInProofreadingToggleVisibility(); messageTable.updateLastMessage(); }, /** * The scroll handler */ scroll: function () { var $window, isActionBarFloating, needsTableHeaderFloat, needsTableHeaderStick, needsActionBarFloat, needsActionBarStick, windowScrollTop, windowScrollBottom, messageTableRelativePos, messageListOffset, messageListHeight, messageListWidth, messageListTop, messageListBottom; $window = $( window ); windowScrollTop = $window.scrollTop(); windowScrollBottom = windowScrollTop + $window.height(); messageListOffset = this.$container.offset(); messageListHeight = this.$container.height(); messageListTop = messageListOffset.top; messageListBottom = messageListTop + messageListHeight; messageListWidth = this.$container.width(); // Header: messageTableRelativePos = messageListTop - this.$header.height() - windowScrollTop; needsTableHeaderFloat = messageTableRelativePos + 10 < 0; needsTableHeaderStick = messageTableRelativePos - 10 >= 0; if ( needsTableHeaderFloat ) { this.$header.addClass( 'floating' ).width( messageListWidth ); } else if ( needsTableHeaderStick ) { // Let the element change width automatically again this.$header.removeClass( 'floating' ).css( 'width', '' ); } // Action bar: isActionBarFloating = this.$actionBar.hasClass( 'floating' ); needsActionBarFloat = windowScrollBottom < messageListBottom; needsActionBarStick = windowScrollBottom > ( messageListBottom + this.$actionBar.height() ); if ( !isActionBarFloating && needsActionBarFloat ) { this.$actionBar.addClass( 'floating' ).width( messageListWidth ); } else if ( isActionBarFloating && needsActionBarStick ) { // Let the element change width automatically again this.$actionBar.removeClass( 'floating' ).css( 'width', '' ); } else if ( isActionBarFloating && needsActionBarFloat ) { this.$actionBar.width( messageListWidth ); } } }; /* * messagetable PLUGIN DEFINITION */ $.fn.messagetable = function ( options ) { return this.each( function () { var $this = $( this ), data = $this.data( 'messagetable' ); if ( !data ) { $this.data( 'messagetable', ( data = new MessageTable( this, options ) ) ); } if ( typeof options === 'string' ) { data[ options ].call( $this ); } } ); }; $.fn.messagetable.Constructor = MessageTable; $.fn.messagetable.defaults = { mode: new mw.Uri().query.action || 'translate' }; /** * Escape the search query for regex match. * * @param {string} value A search string to be escaped. * @return {string} Escaped string that is safe to use for a search. */ function escapeRegex( value ) { return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' ); } function isLoaderVisible( $loader ) { var viewportBottom, elementTop, $window = $( window ); viewportBottom = ( window.innerHeight ? window.innerHeight : $window.height() ) + $window.scrollTop(); elementTop = $loader.offset().top; // Start already if user is reaching close to the bottom return elementTop - viewportBottom < 200; } }( jQuery, mediaWiki ) );