diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 5e992599f89..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -dist/**/* -external/**/* -tests/lib/vendor/**/* -ui/vendor/**/* diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index e7d67eb0e51..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "root": true, - - "extends": "jquery", - - // Uncomment to find useless comment disable directives - // "reportUnusedDisableDirectives": true, - - "parserOptions": { - "ecmaVersion": 2018 - }, - - "env": { - "es6": true, - "node": true - }, - - "rules": { - "strict": [ "error", "global" ] - } -} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e7c145c0a6d..5344af32438 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,7 +33,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java @@ -41,7 +41,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -55,4 +55,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 diff --git a/.github/workflows/filestash.yml b/.github/workflows/filestash.yml index 7716025a081..9f0960a4a98 100644 --- a/.github/workflows/filestash.yml +++ b/.github/workflows/filestash.yml @@ -20,12 +20,12 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Cache - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 6279223a35b..cccd059d28e 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -34,12 +34,12 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Cache - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }} @@ -49,12 +49,13 @@ jobs: - name: Install npm dependencies run: npm install - - name: Lint - run: npm run lint - - name: Build run: npm run build + # Lint must happen after build as we lint generated files. + - name: Lint + run: npm run lint + - name: Test run: | npm run test:unit -- \ @@ -78,12 +79,12 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Cache - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }} @@ -116,12 +117,12 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Cache - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }} diff --git a/Gruntfile.js b/Gruntfile.js index 334e4bb1ae1..bbb71d33e52 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -51,9 +51,6 @@ const cssFiles = [ // minified files const minify = { - options: { - preserveComments: false - }, main: { options: { banner: createBanner( uiFiles ) @@ -174,7 +171,7 @@ grunt.initConfig( { } }, - uglify: minify, + minify, htmllint: { good: { options: { @@ -207,9 +204,12 @@ grunt.initConfig( { "ui/**/*.js", "!ui/vendor/**/*.js", "Gruntfile.js", + "dist/jquery-ui.js", + "dist/jquery-ui.min.js", "build/**/*.js", "tests/unit/**/*.js", "tests/lib/**/*.js", + "!tests/lib/vendor/**/*.js", "demos/**/*.js" ] }, @@ -403,9 +403,9 @@ grunt.registerTask( "lint", [ "csslint", "htmllint" ] ); -grunt.registerTask( "build", [ "requirejs", "concat" ] ); -grunt.registerTask( "default", [ "lint", "build" ] ); -grunt.registerTask( "sizer", [ "requirejs:js", "uglify:main", "compare_size:all" ] ); -grunt.registerTask( "sizer_all", [ "requirejs:js", "uglify", "compare_size" ] ); +grunt.registerTask( "build", [ "requirejs", "concat", "minify:main" ] ); +grunt.registerTask( "default", [ "build", "lint" ] ); +grunt.registerTask( "sizer", [ "requirejs:js", "minify:main", "compare_size:all" ] ); +grunt.registerTask( "sizer_all", [ "requirejs:js", "minify", "compare_size" ] ); }; diff --git a/bower.json b/bower.json index eec454dea05..eb3187e0c38 100644 --- a/bower.json +++ b/bower.json @@ -13,7 +13,7 @@ }, "devDependencies": { "jquery-color": "3.0.0", - "jquery-mousewheel": "3.1.12", + "jquery-mousewheel": "3.2.2", "jquery-simulate": "1.1.1", "qunit": "2.19.4", "requirejs": "2.1.14", diff --git a/build/tasks/minify.js b/build/tasks/minify.js new file mode 100644 index 00000000000..6d83831ee3c --- /dev/null +++ b/build/tasks/minify.js @@ -0,0 +1,53 @@ +"use strict"; + +const swc = require( "@swc/core" ); + +module.exports = function( grunt ) { + +grunt.registerMultiTask( "minify", async function() { + const done = this.async(); + const options = this.options(); + + for ( const file of this.files ) { + if ( file.src.length === 0 ) { + grunt.log.writeln( + `No source file found, skipping minification to "${ file.dest }".` ); + continue; + } + if ( file.src.length !== 1 ) { + grunt.fail.warn( "Minifying multiple source files into one " + + "destination file not supported" ); + } + + const contents = grunt.file.read( file.src[ 0 ] ); + + const { code } = await swc.minify( + contents, + { + compress: { + ecma: 5, + hoist_funs: false, + loops: false + }, + format: { + ecma: 5, + asciiOnly: true, + comments: false, + preamble: options.banner + }, + inlineSourcesContent: false, + mangle: true, + module: false, + sourceMap: false + } + ); + + grunt.file.write( file.dest, code ); + + grunt.log.writeln( `File ${ file.dest } created.` ); + } + + done(); +} ); + +}; diff --git a/demos/.eslintrc.json b/demos/.eslintrc.json deleted file mode 100644 index 805ec8eb261..00000000000 --- a/demos/.eslintrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "root": true, - - "extends": "../ui/.eslintrc.json", - - "globals": { - "require": true - } -} diff --git a/demos/spinner/currency.html b/demos/spinner/currency.html index 4180b12e111..fa3744ba51c 100644 --- a/demos/spinner/currency.html +++ b/demos/spinner/currency.html @@ -7,7 +7,7 @@ - - - - - - + + + + + + + + + + + diff --git a/tests/unit/tabs/core.js b/tests/unit/tabs/core.js index c2fd8904884..1eac3c26834 100644 --- a/tests/unit/tabs/core.js +++ b/tests/unit/tabs/core.js @@ -747,4 +747,81 @@ QUnit.test( "extra listeners created when tabs are added/removed (trac-15136)", "No extra listeners after removing all the extra tabs" ); } ); +QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) { + assert.expect( 1 ); + + var origAjax = $.ajax, + element = $( "#tabs1" ), + anchor = element.find( "a[href='#fragment-3']" ), + url = new URL( anchor.prop( "href" ) ); + + try { + $.ajax = function() { + throw new Error( "Unexpected AJAX call; all tabs are local!" ); + }; + + anchor.attr( "href", url.protocol + "//username:password@" + url.host + + url.pathname + url.search + url.hash ); + + element.tabs(); + anchor.trigger( "click" ); + + assert.strictEqual( element.tabs( "option", "active" ), 2, + "should set the active option" ); + } finally { + $.ajax = origAjax; + } +} ); + +( function() { + function getVerifyTab( assert, element ) { + return function verifyTab( index ) { + assert.strictEqual( + element.tabs( "option", "active" ), + index, + "should set the active option to " + index ); + assert.strictEqual( + element.find( "[role='tabpanel']:visible" ).text().trim(), + "Tab " + ( index + 1 ), + "should set the panel to 'Tab " + ( index + 1 ) + "'" ); + }; + } + + QUnit.test( "href encoding/decoding (gh-2344)", function( assert ) { + assert.expect( 12 ); + + location.hash = "#tabs-2"; + + var i, + element = $( "#tabs10" ).tabs(), + tabLinks = element.find( "> ul a" ), + verifyTab = getVerifyTab( assert, element ); + + for ( i = 0; i < tabLinks.length; i++ ) { + tabLinks.eq( i ).trigger( "click" ); + verifyTab( i ); + } + + location.hash = ""; + } ); + + QUnit.test( "href encoding/decoding on init (gh-2344)", function( assert ) { + assert.expect( 12 ); + + var i, + element = $( "#tabs10" ), + tabLinks = element.find( "> ul a" ), + verifyTab = getVerifyTab( assert, element ); + + for ( i = 0; i < tabLinks.length; i++ ) { + location.hash = tabLinks.eq( i ).attr( "href" ); + element.tabs(); + verifyTab( i ); + element.tabs( "destroy" ); + } + + location.hash = ""; + } ); +} )(); + } ); diff --git a/tests/unit/tabs/tabs.html b/tests/unit/tabs/tabs.html index cb4e5389f62..3f18fa015f6 100644 --- a/tests/unit/tabs/tabs.html +++ b/tests/unit/tabs/tabs.html @@ -125,6 +125,35 @@
+
+ +
+

Tab 1

+
+
+

Tab 2

+
+
+

Tab 3

+
+
+

Tab 4

+
+
+

Tab 5

+
+
+

Tab 6

+
+
+ diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json deleted file mode 100644 index eaba81af2a2..00000000000 --- a/ui/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "root": true, - - "extends": "jquery", - - "parserOptions": { - "ecmaVersion": 5 - }, - - "env": { - "browser": true, - "jquery": true, - "node": false - }, - - "rules": { - "strict": [ "error", "function" ], - - // The following rule is relaxed due to too many violations: - "no-unused-vars": [ "error", { "vars": "all", "args": "after-used" } ], - - // Too many violations: - "camelcase": "off", - "no-nested-ternary": "off" - }, - - "globals": { - "define": false, - "Globalize": false - }, - - "overrides": [ - { - "files": [ "i18n/**/*.js" ], - "rules": { - - // We want to keep all the strings in separate single lines - "max-len": "off" - } - } - ] -} diff --git a/ui/effect.js b/ui/effect.js index bbbb733c3db..cb9ab804331 100644 --- a/ui/effect.js +++ b/ui/effect.js @@ -9,9 +9,7 @@ //>>label: Effects Core //>>group: Effects -/* eslint-disable max-len */ //>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects. -/* eslint-enable max-len */ //>>docs: https://api.jqueryui.com/category/effects-core/ //>>demos: https://jqueryui.com/effect/ @@ -320,7 +318,7 @@ if ( $.uiBackCompat === true ) { try { // eslint-disable-next-line no-unused-expressions active.id; - } catch ( e ) { + } catch ( _e ) { active = document.body; } diff --git a/ui/effects/effect-explode.js b/ui/effects/effect-explode.js index ed40833a89a..da38b4d5565 100644 --- a/ui/effects/effect-explode.js +++ b/ui/effects/effect-explode.js @@ -9,9 +9,7 @@ //>>label: Explode Effect //>>group: Effects -/* eslint-disable max-len */ //>>description: Explodes an element in all directions into n pieces. Implodes an element to its original wholeness. -/* eslint-enable max-len */ //>>docs: https://api.jqueryui.com/explode-effect/ //>>demos: https://jqueryui.com/effect/ diff --git a/ui/widgets/accordion.js b/ui/widgets/accordion.js index ff6e4631da8..43a50db8319 100644 --- a/ui/widgets/accordion.js +++ b/ui/widgets/accordion.js @@ -9,9 +9,7 @@ //>>label: Accordion //>>group: Widgets -/* eslint-disable max-len */ //>>description: Displays collapsible content panels for presenting information in a limited amount of space. -/* eslint-enable max-len */ //>>docs: https://api.jqueryui.com/accordion/ //>>demos: https://jqueryui.com/accordion/ //>>css.structure: ../../themes/base/core.css diff --git a/ui/widgets/datepicker.js b/ui/widgets/datepicker.js index 323723b8939..029f255e87e 100644 --- a/ui/widgets/datepicker.js +++ b/ui/widgets/datepicker.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len, camelcase */ +/* eslint-disable max-len */ /*! * jQuery UI Datepicker @VERSION * https://jqueryui.com @@ -535,7 +535,7 @@ $.extend( Datepicker.prototype, { _getInst: function( target ) { try { return $.data( target, "datepicker" ); - } catch ( err ) { + } catch ( _err ) { throw "Missing instance data for this datepicker"; } }, @@ -768,7 +768,7 @@ $.extend( Datepicker.prototype, { $.datepicker._updateAlternate( inst ); $.datepicker._updateDatepicker( inst ); } - } catch ( err ) { + } catch ( _err ) { } } return true; @@ -1540,7 +1540,7 @@ $.extend( Datepicker.prototype, { try { date = this.parseDate( dateFormat, dates, settings ) || defaultDate; - } catch ( event ) { + } catch ( _err ) { dates = ( noDefault ? "" : dates ); } inst.selectedDay = date.getDate(); @@ -1569,7 +1569,7 @@ $.extend( Datepicker.prototype, { try { return $.datepicker.parseDate( $.datepicker._get( inst, "dateFormat" ), offset, $.datepicker._getFormatConfig( inst ) ); - } catch ( e ) { + } catch ( _e ) { // Ignore } diff --git a/ui/widgets/progressbar.js b/ui/widgets/progressbar.js index 20e96440a0c..ad5366adeae 100644 --- a/ui/widgets/progressbar.js +++ b/ui/widgets/progressbar.js @@ -9,9 +9,7 @@ //>>label: Progressbar //>>group: Widgets -/* eslint-disable max-len */ //>>description: Displays a status indicator for loading state, standard percentage, and other progress indicators. -/* eslint-enable max-len */ //>>docs: https://api.jqueryui.com/progressbar/ //>>demos: https://jqueryui.com/progressbar/ //>>css.structure: ../../themes/base/core.css diff --git a/ui/widgets/resizable.js b/ui/widgets/resizable.js index 6f1e0ebdecb..01231528314 100644 --- a/ui/widgets/resizable.js +++ b/ui/widgets/resizable.js @@ -104,7 +104,7 @@ $.widget( "ui.resizable", $.ui.mouse, { el[ scroll ] = 1; has = ( el[ scroll ] > 0 ); el[ scroll ] = 0; - } catch ( e ) { + } catch ( _e ) { // `el` might be a string, then setting `scroll` will throw // an error in strict mode; ignore it. diff --git a/ui/widgets/selectmenu.js b/ui/widgets/selectmenu.js index f1b48fa6032..749e3349194 100644 --- a/ui/widgets/selectmenu.js +++ b/ui/widgets/selectmenu.js @@ -9,9 +9,7 @@ //>>label: Selectmenu //>>group: Widgets -/* eslint-disable max-len */ //>>description: Duplicates and extends the functionality of a native HTML select element, allowing it to be customizable in behavior and appearance far beyond the limitations of a native select. -/* eslint-enable max-len */ //>>docs: https://api.jqueryui.com/selectmenu/ //>>demos: https://jqueryui.com/selectmenu/ //>>css.structure: ../../themes/base/core.css diff --git a/ui/widgets/spinner.js b/ui/widgets/spinner.js index d999d85d783..d4034b4589d 100644 --- a/ui/widgets/spinner.js +++ b/ui/widgets/spinner.js @@ -136,9 +136,10 @@ $.widget( "ui.spinner", { this._trigger( "change", event ); } }, - mousewheel: function( event, delta ) { + wheel: function( event ) { var activeElement = this.document[ 0 ].activeElement; var isActive = this.element[ 0 ] === activeElement; + var delta = event.deltaY || event.originalEvent && event.originalEvent.deltaY; if ( !isActive || !delta ) { return; @@ -148,7 +149,7 @@ $.widget( "ui.spinner", { return false; } - this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event ); + this._spin( ( delta > 0 ? -1 : 1 ) * this.options.step, event ); clearTimeout( this.mousewheelTimer ); this.mousewheelTimer = this._delay( function() { if ( this.spinning ) { @@ -157,6 +158,27 @@ $.widget( "ui.spinner", { }, 100 ); event.preventDefault(); }, + + // DEPRECATED + // Kept for backwards compatibility. Please use the modern `wheel` + // event. The `delta` parameter is provided by the jQuery Mousewheel + // plugin if one is loaded. + mousewheel: function( event, delta ) { + if ( !event.isTrigger ) { + + // If this is not a trigger call, the `wheel` handler will + // fire as well, let's not duplicate it. + return; + } + + var wheelEvent = $.Event( event ); + wheelEvent.type = "wheel"; + if ( delta ) { + wheelEvent.deltaY = -delta; + } + return this._events.wheel.call( this, wheelEvent ); + }, + "mousedown .ui-spinner-button": function( event ) { var previous; diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js index 7b7907c3277..494e54f2243 100644 --- a/ui/widgets/tabs.js +++ b/ui/widgets/tabs.js @@ -61,26 +61,19 @@ $.widget( "ui.tabs", { load: null }, - _isLocal: ( function() { - var rhash = /#.*$/; - - return function( anchor ) { - var anchorUrl, locationUrl; - - anchorUrl = anchor.href.replace( rhash, "" ); - locationUrl = location.href.replace( rhash, "" ); - - // Decoding may throw an error if the URL isn't UTF-8 (#9518) - try { - anchorUrl = decodeURIComponent( anchorUrl ); - } catch ( error ) {} - try { - locationUrl = decodeURIComponent( locationUrl ); - } catch ( error ) {} - - return anchor.hash.length > 1 && anchorUrl === locationUrl; - }; - } )(), + _isLocal: function( anchor ) { + var anchorUrl = new URL( anchor.href ), + locationUrl = new URL( location.href ); + + return anchor.hash.length > 1 && + + // `href` may contain a hash but also username & password; + // we want to ignore them, so we check the three fields + // below instead. + anchorUrl.origin === locationUrl.origin && + anchorUrl.pathname === locationUrl.pathname && + anchorUrl.search === locationUrl.search; + }, _create: function() { var that = this, @@ -121,18 +114,31 @@ $.widget( "ui.tabs", { _initialActive: function() { var active = this.options.active, collapsible = this.options.collapsible, - locationHashDecoded = decodeURIComponent( location.hash.substring( 1 ) ); + locationHash = location.hash.substring( 1 ), + locationHashDecoded = decodeURIComponent( locationHash ); if ( active === null ) { // check the fragment identifier in the URL - if ( locationHashDecoded ) { + if ( locationHash ) { this.tabs.each( function( i, tab ) { - if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { active = i; return false; } } ); + + // If not found, decode the hash & try again. + // See the comment in `_processTabs` under the `_isLocal` check + // for more information. + if ( active === null ) { + this.tabs.each( function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) { + active = i; + return false; + } + } ); + } } // Check for a tab marked active via a class @@ -430,9 +436,24 @@ $.widget( "ui.tabs", { // Inline tab if ( that._isLocal( anchor ) ) { - selector = decodeURIComponent( anchor.hash ); + + // The "scrolling to a fragment" section of the HTML spec: + // https://html.spec.whatwg.org/#scrolling-to-a-fragment + // uses a concept of document's indicated part: + // https://html.spec.whatwg.org/#the-indicated-part-of-the-document + // Slightly below there's an algorithm to compute the indicated + // part: + // https://html.spec.whatwg.org/#the-indicated-part-of-the-document + // First, the algorithm tries the hash as-is, without decoding. + // Then, if one is not found, the same is attempted with a decoded + // hash. Replicate this logic. + selector = anchor.hash; panelId = selector.substring( 1 ); panel = that.element.find( "#" + CSS.escape( panelId ) ); + if ( !panel.length ) { + panelId = decodeURIComponent( panelId ); + panel = that.element.find( "#" + CSS.escape( panelId ) ); + } // remote tab } else {