diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..e6fa251
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+/i18n/
+/node_modules/
+/vendor/
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..04f9da7
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "root": true,
+ "extends": [
+ "wikimedia/server"
+ ]
+}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a174ffa
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..c81da42
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,119 @@
+name: Lint
+
+on:
+ push:
+ branches:
+ - "**"
+ - "!**dependabot/**"
+ - "!**release-please--**"
+ pull_request:
+ branches:
+ - "**"
+ - "!**dependabot/**"
+ - "!**release-please--**"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # OR "2" -> To retrieve the preceding commit.
+
+ - name: Cache dependencies
+ uses: actions/cache@v4
+ with:
+ path: /home/runner/cache
+ key: ${{ runner.os }}-${{ hashFiles('**/*.lock') }}
+
+ - name: Install dependencies
+ run: |
+ composer validate
+ composer install --prefer-dist --no-progress
+ npm install --save-dev
+
+ # Check for changed files
+ - name: Check for PHP changes
+ id: changed-php
+ uses: tj-actions/changed-files@v45
+ with:
+ since_last_remote_commit: "true"
+ files: |
+ includes/**/*.php
+
+ - name: Check for script changes
+ id: changed-script
+ uses: tj-actions/changed-files@v45
+ with:
+ since_last_remote_commit: "true"
+ files: |
+ resources/**/*.js
+
+ - name: Check for stylesheet changes
+ id: changed-stylesheet
+ uses: tj-actions/changed-files@v45
+ with:
+ since_last_remote_commit: "true"
+ files: |
+ resources/**/*.css
+ resources/**/*.less
+
+ - name: Check for i18n changes
+ id: changed-i18n
+ uses: tj-actions/changed-files@v45
+ with:
+ since_last_remote_commit: "true"
+ files: |
+ i18n/*.json
+
+ # Bypass the phpcbf non-standard exit code
+ - name: Lint PHP
+ if: steps.changed-php.outputs.any_changed == 'true'
+ continue-on-error: true
+ run: |
+ sh ./bin/phpcbf.sh
+ composer fix
+ composer test
+
+ - name: Lint script
+ if: steps.changed-script.outputs.any_changed == 'true'
+ continue-on-error: true
+ run: |
+ npm run lint:fix:js
+ npm run lint:js
+
+ - name: Lint stylesheet
+ if: steps.changed-stylesheet.outputs.any_changed == 'true'
+ continue-on-error: true
+ run: |
+ npm run lint:fix:styles
+ npm run lint:styles
+
+ - name: Lint i18n
+ if: steps.changed-i18n.outputs.any_changed == 'true'
+ continue-on-error: true
+ run: |
+ npm run lint:i18n
+
+ # Only patch code when it is a push event
+ - name: Push the changes
+ if: github.event_name == 'push'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ if ! git diff --exit-code --quiet; then
+ git config --global user.name "github-actions"
+ git config --global user.email "github-actions@users.noreply.github.com"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
+ git add .
+ git commit -am "ci: 👷 lint code to MediaWiki standards" -m "Check commit and GitHub actions for more details"
+ git pull --rebase
+ git push
+ else
+ echo "No changes to commit"
+ fi
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 0000000..7179ce4
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,18 @@
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: write
+
+name: release-please
+
+jobs:
+ release-please:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: googleapis/release-please-action@v4
+ with:
+ token: ${{ secrets.WORKFLOW_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bfd3338
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/composer.lock
+/vendor
+/node_modules
+.eslintcache
\ No newline at end of file
diff --git a/.phan/config.php b/.phan/config.php
new file mode 100644
index 0000000..76efd06
--- /dev/null
+++ b/.phan/config.php
@@ -0,0 +1,3 @@
+
+
+
+ .
+
+
+ */vendor/*
+ */out/*
+
\ No newline at end of file
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..9987e47
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.0.1"
+}
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000..25abe76
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,9 @@
+{
+ "extends": [
+ "stylelint-config-idiomatic-order",
+ "stylelint-config-wikimedia"
+ ],
+ "rules": {
+ "selector-max-id": null
+ }
+}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..498acf7
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1 @@
+The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Special:MyLanguage/Code_of_Conduct).
diff --git a/FloatingUI.magic.php b/FloatingUI.magic.php
new file mode 100644
index 0000000..a6236b0
--- /dev/null
+++ b/FloatingUI.magic.php
@@ -0,0 +1,7 @@
+ [ 0, 'floatingui' ]
+];
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index f288702..94a9ed0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
- Copyright (C) 2007 Free Software Foundation, Inc.
+ Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
- along with this program. If not, see .
+ along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
-.
+.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
-.
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bf40dd5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# FloatingUI
+
+The FloatingUI extension implements [Floating UI](https://floating-ui.com) library in MediaWiki.
+
+[Extension:FloatingUI on MediaWiki](https://www.mediawiki.org/wiki/Extension:FloatingUI).
+
+## Requirements
+* [MediaWiki](https://www.mediawiki.org) 1.39 or later
+
+## Installation
+You can get the extension via Git (specifying FloatingUI as the destination directory):
+
+ git clone https://github.com/StarCitizenTools/mediawiki-extensions-FloatingUI.git FloatingUI
+
+Or [download it as zip archive](https://github.com/StarCitizenTools/mediawiki-extensions-FloatingUI/archive/main.zip).
+
+In either case, the "FloatingUI" extension should end up in the "extensions" directory
+of your MediaWiki installation. If you got the zip archive, you will need to put it
+into a directory called FloatingUI.
\ No newline at end of file
diff --git a/bin/phpcbf.sh b/bin/phpcbf.sh
new file mode 100644
index 0000000..7226f7a
--- /dev/null
+++ b/bin/phpcbf.sh
@@ -0,0 +1,8 @@
+# Force phpcbf to exit with zero
+# See https://github.com/squizlabs/PHP_CodeSniffer/issues/1818#issuecomment-354420927
+
+root=$( dirname $0 )/..
+
+$root/vendor/bin/phpcbf
+
+exit 0
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..e159a3a
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,54 @@
+{
+ "name": "starcitizentools/floating-ui",
+ "type": "mediawiki-extension",
+ "description": "Implements the FloatingUI Javascript library.",
+ "keywords": [
+ "MediaWiki",
+ "FloatingUI",
+ "extension"
+ ],
+ "homepage": "https://www.mediawiki.org/wiki/Extension:FloatingUI",
+ "readme": "README.md",
+ "license": "GPL-3.0-or-later",
+ "authors": [
+ {
+ "name": "alistair3149",
+ "email": "alistair31494322@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "support": {
+ "forum": "https://www.mediawiki.org/wiki/Extension_talk:FloatingUI",
+ "wiki": "https://www.mediawiki.org/wiki/Extension:FloatingUI",
+ "source": "https://github.com/StarCitizenTools/mediawiki-extensions-FloatingUI"
+ },
+ "require": {
+ "composer/installers": ">=1.0.1"
+ },
+ "require-dev": {
+ "mediawiki/mediawiki-codesniffer": "44.0.0",
+ "mediawiki/mediawiki-phan-config": "0.14.0",
+ "mediawiki/minus-x": "1.1.3",
+ "php-parallel-lint/php-console-highlighter": "1.0.0",
+ "php-parallel-lint/php-parallel-lint": "1.4.0"
+ },
+ "scripts": {
+ "fix": [
+ "minus-x fix .",
+ "phpcbf"
+ ],
+ "test": [
+ "parallel-lint . --exclude vendor --exclude node_modules",
+ "phpcs --config-set ignore_warnings_on_exit 1",
+ "phpcs -p -s",
+ "minus-x check ."
+ ],
+ "phan": "phan -d . --long-progress-bar"
+ },
+ "config": {
+ "allow-plugins": {
+ "composer/installers": true,
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ }
+}
diff --git a/extension.json b/extension.json
new file mode 100644
index 0000000..5906d7a
--- /dev/null
+++ b/extension.json
@@ -0,0 +1,53 @@
+{
+ "name": "FloatingUI",
+ "version": "0.0.1",
+ "author": [
+ "alistair3149"
+ ],
+ "url": "https://www.mediawiki.org/wiki/Extension:FloatingUI",
+ "descriptionmsg": "floatingui-desc",
+ "type": "parserhook",
+ "license-name": "GPL-3.0-or-later",
+ "requires": {
+ "MediaWiki": ">= 1.39.0"
+ },
+ "MessagesDirs": {
+ "FloatingUI": [
+ "/i18n"
+ ]
+ },
+ "ExtensionMessagesFiles": {
+ "FloatingUIMagic": "FloatingUI.magic.php"
+ },
+ "AutoloadNamespaces": {
+ "MediaWiki\\Extension\\FloatingUI\\": "includes/"
+ },
+ "ResourceModules": {
+ "ext.floatingUI.lib": {
+ "scripts": [
+ "ext.floatingUI.lib/core.js",
+ "ext.floatingUI.lib/dom.js"
+ ]
+ }
+ },
+ "ResourceFileModulePaths": {
+ "localBasePath": "modules",
+ "remoteExtPath": "FloatingUI/modules"
+ },
+ "Hooks": {
+ "ParserFirstCallInit": "parser"
+ },
+ "HookHandlers": {
+ "parser": {
+ "class": "MediaWiki\\Extension\\FloatingUI\\Hooks"
+ }
+ },
+ "attributes": {
+ "CodeMirror": {
+ "TagModes": {
+ "floatingui": "text/mediawiki"
+ }
+ }
+ },
+ "manifest_version": 2
+}
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..2c9e814
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "alistair3149"
+ ]
+ },
+ "floatingui-desc": "Implements the FloatingUI Javascript library."
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100644
index 0000000..d28c40b
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "alistair3149"
+ ]
+ },
+ "floatingui-desc": "{{desc|name=FloatingUI|url=http://www.mediawiki.org/wiki/Extension:FloatingUI}}"
+}
diff --git a/includes/FloatingUI.php b/includes/FloatingUI.php
new file mode 100644
index 0000000..872a701
--- /dev/null
+++ b/includes/FloatingUI.php
@@ -0,0 +1,44 @@
+expand( $args[$key] ) : '';
+ }
+
+ private static function parseWikitext( $wikitext, $parser, $frame ) {
+ $wikitext = $parser->recursiveTagParseFully( $wikitext, $frame );
+ $wikitext = $parser->stripOuterParagraph( $wikitext );
+ return $wikitext;
+ }
+
+ public static function parserHook( Parser $parser, PPFrame $frame, $args ) {
+ // Load Floating UI library
+ $parser->getOutput()->addModules( [ 'ext.floatingUI.lib' ] );
+
+ /*
+ $referenceArg = self::getArg( 0, $args, $frame );
+ $contentArg = self::getArg( 1, $args, $frame );
+
+ // Parse into wikitext into HTML because we are not parsing the output
+ $referenceHtml = self::parseWikitext( $referenceArg, $parser, $frame );
+ $contentHtml = self::parseWikitext( $contentArg, $parser, $frame );
+
+ $isInline = true;
+ $wrapperTag = $isInline ? 'span' : 'div';
+
+ $output = <<$referenceHtml$wrapperTag>
+ <$wrapperTag class='ext-floatingui-content'>$contentHtml$wrapperTag>
+ HTML;
+
+ return [ $output, 'noparse' => true, 'isHTML' => true ];
+ */
+ }
+}
diff --git a/includes/Hooks.php b/includes/Hooks.php
new file mode 100644
index 0000000..27e5d44
--- /dev/null
+++ b/includes/Hooks.php
@@ -0,0 +1,19 @@
+setFunctionHook( 'floatingui', [ FloatingUI::class, 'parserHook' ], Parser::SFH_OBJECT_ARGS );
+ }
+}
diff --git a/modules/.eslintrc.json b/modules/.eslintrc.json
new file mode 100644
index 0000000..d059a34
--- /dev/null
+++ b/modules/.eslintrc.json
@@ -0,0 +1,19 @@
+{
+ "extends": [
+ "wikimedia/client-es6",
+ "wikimedia/mediawiki"
+ ],
+ "env": {
+ "browser": true
+ },
+ "globals": {
+ "exports": true
+ },
+ "parserOptions": {
+ "sourceType": "module"
+ },
+ "rules": {
+ "jsdoc/no-undefined-types": "off",
+ "max-len": "off"
+ }
+}
diff --git a/modules/ext.floatingUI.lib/core.js b/modules/ext.floatingUI.lib/core.js
new file mode 100644
index 0000000..9d9abf0
--- /dev/null
+++ b/modules/ext.floatingUI.lib/core.js
@@ -0,0 +1,9 @@
+/* eslint-disable */
+/*
+ * Floating UI Core
+ * @see https://cdn.jsdelivr.net/npm/@floating-ui/core@1.6.7
+ *
+ * TODO: Use Foreign resources when dropping MW 1.39 support
+ * @see https://www.mediawiki.org/wiki/ResourceLoader/Foreign_resources
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).FloatingUICore={})}(this,(function(t){"use strict";const e=["top","right","bottom","left"],n=["start","end"],i=e.reduce(((t,e)=>t.concat(e,e+"-"+n[0],e+"-"+n[1])),[]),o=Math.min,r=Math.max,a={left:"right",right:"left",bottom:"top",top:"bottom"},l={start:"end",end:"start"};function s(t,e,n){return r(t,o(e,n))}function f(t,e){return"function"==typeof t?t(e):t}function c(t){return t.split("-")[0]}function u(t){return t.split("-")[1]}function m(t){return"x"===t?"y":"x"}function d(t){return"y"===t?"height":"width"}function g(t){return["top","bottom"].includes(c(t))?"y":"x"}function p(t){return m(g(t))}function h(t,e,n){void 0===n&&(n=!1);const i=u(t),o=p(t),r=d(o);let a="x"===o?i===(n?"end":"start")?"right":"left":"start"===i?"bottom":"top";return e.reference[r]>e.floating[r]&&(a=w(a)),[a,w(a)]}function y(t){return t.replace(/start|end/g,(t=>l[t]))}function w(t){return t.replace(/left|right|bottom|top/g,(t=>a[t]))}function x(t){return"number"!=typeof t?function(t){return{top:0,right:0,bottom:0,left:0,...t}}(t):{top:t,right:t,bottom:t,left:t}}function v(t){const{x:e,y:n,width:i,height:o}=t;return{width:i,height:o,top:n,left:e,right:e+i,bottom:n+o,x:e,y:n}}function b(t,e,n){let{reference:i,floating:o}=t;const r=g(e),a=p(e),l=d(a),s=c(e),f="y"===r,m=i.x+i.width/2-o.width/2,h=i.y+i.height/2-o.height/2,y=i[l]/2-o[l]/2;let w;switch(s){case"top":w={x:m,y:i.y-o.height};break;case"bottom":w={x:m,y:i.y+i.height};break;case"right":w={x:i.x+i.width,y:h};break;case"left":w={x:i.x-o.width,y:h};break;default:w={x:i.x,y:i.y}}switch(u(e)){case"start":w[a]-=y*(n&&f?-1:1);break;case"end":w[a]+=y*(n&&f?-1:1)}return w}async function A(t,e){var n;void 0===e&&(e={});const{x:i,y:o,platform:r,rects:a,elements:l,strategy:s}=t,{boundary:c="clippingAncestors",rootBoundary:u="viewport",elementContext:m="floating",altBoundary:d=!1,padding:g=0}=f(e,t),p=x(g),h=l[d?"floating"===m?"reference":"floating":m],y=v(await r.getClippingRect({element:null==(n=await(null==r.isElement?void 0:r.isElement(h)))||n?h:h.contextElement||await(null==r.getDocumentElement?void 0:r.getDocumentElement(l.floating)),boundary:c,rootBoundary:u,strategy:s})),w="floating"===m?{x:i,y:o,width:a.floating.width,height:a.floating.height}:a.reference,b=await(null==r.getOffsetParent?void 0:r.getOffsetParent(l.floating)),A=await(null==r.isElement?void 0:r.isElement(b))&&await(null==r.getScale?void 0:r.getScale(b))||{x:1,y:1},R=v(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:l,rect:w,offsetParent:b,strategy:s}):w);return{top:(y.top-R.top+p.top)/A.y,bottom:(R.bottom-y.bottom+p.bottom)/A.y,left:(y.left-R.left+p.left)/A.x,right:(R.right-y.right+p.right)/A.x}}function R(t,e){return{top:t.top-e.height,right:t.right-e.width,bottom:t.bottom-e.height,left:t.left-e.width}}function P(t){return e.some((e=>t[e]>=0))}function T(t){const e=o(...t.map((t=>t.left))),n=o(...t.map((t=>t.top)));return{x:e,y:n,width:r(...t.map((t=>t.right)))-e,height:r(...t.map((t=>t.bottom)))-n}}t.arrow=t=>({name:"arrow",options:t,async fn(e){const{x:n,y:i,placement:r,rects:a,platform:l,elements:c,middlewareData:m}=e,{element:g,padding:h=0}=f(t,e)||{};if(null==g)return{};const y=x(h),w={x:n,y:i},v=p(r),b=d(v),A=await l.getDimensions(g),R="y"===v,P=R?"top":"left",T=R?"bottom":"right",D=R?"clientHeight":"clientWidth",O=a.reference[b]+a.reference[v]-w[v]-a.floating[b],E=w[v]-a.reference[v],L=await(null==l.getOffsetParent?void 0:l.getOffsetParent(g));let k=L?L[D]:0;k&&await(null==l.isElement?void 0:l.isElement(L))||(k=c.floating[D]||a.floating[b]);const C=O/2-E/2,B=k/2-A[b]/2-1,H=o(y[P],B),S=o(y[T],B),F=H,j=k-A[b]-S,z=k/2-A[b]/2+C,M=s(F,z,j),V=!m.arrow&&null!=u(r)&&z!==M&&a.reference[b]/2-(zu(e)===t)),...n.filter((e=>u(e)!==t))]:n.filter((t=>c(t)===t))).filter((n=>!t||u(n)===t||!!e&&y(n)!==n))}(p||null,x,w):w,R=await A(e,v),P=(null==(n=l.autoPlacement)?void 0:n.index)||0,T=b[P];if(null==T)return{};const D=h(T,a,await(null==m.isRTL?void 0:m.isRTL(d.floating)));if(s!==T)return{reset:{placement:b[0]}};const O=[R[c(T)],R[D[0]],R[D[1]]],E=[...(null==(o=l.autoPlacement)?void 0:o.overflows)||[],{placement:T,overflows:O}],L=b[P+1];if(L)return{data:{index:P+1,overflows:E},reset:{placement:L}};const k=E.map((t=>{const e=u(t.placement);return[t.placement,e&&g?t.overflows.slice(0,2).reduce(((t,e)=>t+e),0):t.overflows[0],t.overflows]})).sort(((t,e)=>t[1]-e[1])),C=(null==(r=k.filter((t=>t[2].slice(0,u(t[0])?2:3).every((t=>t<=0))))[0])?void 0:r[0])||k[0][0];return C!==s?{data:{index:P+1,overflows:E},reset:{placement:C}}:{}}}},t.computePosition=async(t,e,n)=>{const{placement:i="bottom",strategy:o="absolute",middleware:r=[],platform:a}=n,l=r.filter(Boolean),s=await(null==a.isRTL?void 0:a.isRTL(e));let f=await a.getElementRects({reference:t,floating:e,strategy:o}),{x:c,y:u}=b(f,i,s),m=i,d={},g=0;for(let n=0;nt+"-"+o)),e&&(r=r.concat(r.map(y)))),r}(l,R,b,E));const C=[l,...L],B=await A(e,P),H=[];let S=(null==(i=r.flip)?void 0:i.overflows)||[];if(d&&H.push(B[T]),p){const t=h(o,a,E);H.push(B[t[0]],B[t[1]])}if(S=[...S,{placement:o,overflows:H}],!H.every((t=>t<=0))){var F,j;const t=((null==(F=r.flip)?void 0:F.index)||0)+1,e=C[t];if(e)return{data:{index:t,overflows:S},reset:{placement:e}};let n=null==(j=S.filter((t=>t.overflows[0]<=0)).sort(((t,e)=>t.overflows[1]-e.overflows[1]))[0])?void 0:j.placement;if(!n)switch(v){case"bestFit":{var z;const t=null==(z=S.filter((t=>{if(k){const e=g(t.placement);return e===D||"y"===e}return!0})).map((t=>[t.placement,t.overflows.filter((t=>t>0)).reduce(((t,e)=>t+e),0)])).sort(((t,e)=>t[1]-e[1]))[0])?void 0:z[0];t&&(n=t);break}case"initialPlacement":n=l}if(o!==n)return{reset:{placement:n}}}return{}}}},t.hide=function(t){return void 0===t&&(t={}),{name:"hide",options:t,async fn(e){const{rects:n}=e,{strategy:i="referenceHidden",...o}=f(t,e);switch(i){case"referenceHidden":{const t=R(await A(e,{...o,elementContext:"reference"}),n.reference);return{data:{referenceHiddenOffsets:t,referenceHidden:P(t)}}}case"escaped":{const t=R(await A(e,{...o,altBoundary:!0}),n.floating);return{data:{escapedOffsets:t,escaped:P(t)}}}default:return{}}}}},t.inline=function(t){return void 0===t&&(t={}),{name:"inline",options:t,async fn(e){const{placement:n,elements:i,rects:a,platform:l,strategy:s}=e,{padding:u=2,x:m,y:d}=f(t,e),p=Array.from(await(null==l.getClientRects?void 0:l.getClientRects(i.reference))||[]),h=function(t){const e=t.slice().sort(((t,e)=>t.y-e.y)),n=[];let i=null;for(let t=0;ti.height/2?n.push([o]):n[n.length-1].push(o),i=o}return n.map((t=>v(T(t))))}(p),y=v(T(p)),w=x(u);const b=await l.getElementRects({reference:{getBoundingClientRect:function(){if(2===h.length&&h[0].left>h[1].right&&null!=m&&null!=d)return h.find((t=>m>t.left-w.left&&mt.top-w.top&&d=2){if("y"===g(n)){const t=h[0],e=h[h.length-1],i="top"===c(n),o=t.top,r=e.bottom,a=i?t.left:e.left,l=i?t.right:e.right;return{top:o,bottom:r,left:a,right:l,width:l-a,height:r-o,x:a,y:o}}const t="left"===c(n),e=r(...h.map((t=>t.right))),i=o(...h.map((t=>t.left))),a=h.filter((n=>t?n.left===i:n.right===e)),l=a[0].top,s=a[a.length-1].bottom;return{top:l,bottom:s,left:i,right:e,width:e-i,height:s-l,x:i,y:l}}return y}},floating:i.floating,strategy:s});return a.reference.x!==b.reference.x||a.reference.y!==b.reference.y||a.reference.width!==b.reference.width||a.reference.height!==b.reference.height?{reset:{rects:b}}:{}}}},t.limitShift=function(t){return void 0===t&&(t={}),{options:t,fn(e){const{x:n,y:i,placement:o,rects:r,middlewareData:a}=e,{offset:l=0,mainAxis:s=!0,crossAxis:u=!0}=f(t,e),d={x:n,y:i},p=g(o),h=m(p);let y=d[h],w=d[p];const x=f(l,e),v="number"==typeof x?{mainAxis:x,crossAxis:0}:{mainAxis:0,crossAxis:0,...x};if(s){const t="y"===h?"height":"width",e=r.reference[h]-r.floating[t]+v.mainAxis,n=r.reference[h]+r.reference[t]-v.mainAxis;yn&&(y=n)}if(u){var b,A;const t="y"===h?"width":"height",e=["top","left"].includes(c(o)),n=r.reference[p]-r.floating[t]+(e&&(null==(b=a.offset)?void 0:b[p])||0)+(e?0:v.crossAxis),i=r.reference[p]+r.reference[t]+(e?0:(null==(A=a.offset)?void 0:A[p])||0)-(e?v.crossAxis:0);wi&&(w=i)}return{[h]:y,[p]:w}}}},t.offset=function(t){return void 0===t&&(t=0),{name:"offset",options:t,async fn(e){var n,i;const{x:o,y:r,placement:a,middlewareData:l}=e,s=await async function(t,e){const{placement:n,platform:i,elements:o}=t,r=await(null==i.isRTL?void 0:i.isRTL(o.floating)),a=c(n),l=u(n),s="y"===g(n),m=["left","top"].includes(a)?-1:1,d=r&&s?-1:1,p=f(e,t);let{mainAxis:h,crossAxis:y,alignmentAxis:w}="number"==typeof p?{mainAxis:p,crossAxis:0,alignmentAxis:null}:{mainAxis:0,crossAxis:0,alignmentAxis:null,...p};return l&&"number"==typeof w&&(y="end"===l?-1*w:w),s?{x:y*d,y:h*m}:{x:h*m,y:y*d}}(e,t);return a===(null==(n=l.offset)?void 0:n.placement)&&null!=(i=l.arrow)&&i.alignmentOffset?{}:{x:o+s.x,y:r+s.y,data:{...s,placement:a}}}}},t.rectToClientRect=v,t.shift=function(t){return void 0===t&&(t={}),{name:"shift",options:t,async fn(e){const{x:n,y:i,placement:o}=e,{mainAxis:r=!0,crossAxis:a=!1,limiter:l={fn:t=>{let{x:e,y:n}=t;return{x:e,y:n}}},...u}=f(t,e),d={x:n,y:i},p=await A(e,u),h=g(c(o)),y=m(h);let w=d[y],x=d[h];if(r){const t="y"===y?"bottom":"right";w=s(w+p["y"===y?"top":"left"],w,w-p[t])}if(a){const t="y"===h?"bottom":"right";x=s(x+p["y"===h?"top":"left"],x,x-p[t])}const v=l.fn({...e,[y]:w,[h]:x});return{...v,data:{x:v.x-n,y:v.y-i}}}}},t.size=function(t){return void 0===t&&(t={}),{name:"size",options:t,async fn(e){const{placement:n,rects:i,platform:a,elements:l}=e,{apply:s=(()=>{}),...m}=f(t,e),d=await A(e,m),p=c(n),h=u(n),y="y"===g(n),{width:w,height:x}=i.floating;let v,b;"top"===p||"bottom"===p?(v=p,b=h===(await(null==a.isRTL?void 0:a.isRTL(l.floating))?"start":"end")?"left":"right"):(b=p,v="end"===h?"top":"bottom");const R=x-d.top-d.bottom,P=w-d.left-d.right,T=o(x-d[v],R),D=o(w-d[b],P),O=!e.middlewareData.shift;let E=T,L=D;if(y?L=h||O?o(D,P):P:E=h||O?o(T,R):R,O&&!h){const t=r(d.left,0),e=r(d.right,0),n=r(d.top,0),i=r(d.bottom,0);y?L=w-2*(0!==t||0!==e?t+e:r(d.left,d.right)):E=x-2*(0!==n||0!==i?n+i:r(d.top,d.bottom))}await s({...e,availableWidth:L,availableHeight:E});const k=await a.getDimensions(l.floating);return w!==k.width||x!==k.height?{reset:{rects:!0}}:{}}}}}));
\ No newline at end of file
diff --git a/modules/ext.floatingUI.lib/dom.js b/modules/ext.floatingUI.lib/dom.js
new file mode 100644
index 0000000..7fdb5dd
--- /dev/null
+++ b/modules/ext.floatingUI.lib/dom.js
@@ -0,0 +1,9 @@
+/* eslint-disable */
+/*
+ * Floating UI DOM
+ * @see https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.10
+ *
+ * TODO: Use Foreign resources when dropping MW 1.39 support
+ * @see https://www.mediawiki.org/wiki/ResourceLoader/Foreign_resources
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@floating-ui/core")):"function"==typeof define&&define.amd?define(["exports","@floating-ui/core"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).FloatingUIDOM={},t.FloatingUICore)}(this,(function(t,e){"use strict";const n=Math.min,o=Math.max,i=Math.round,r=Math.floor,c=t=>({x:t,y:t});function l(t){return u(t)?(t.nodeName||"").toLowerCase():"#document"}function s(t){var e;return(null==t||null==(e=t.ownerDocument)?void 0:e.defaultView)||window}function f(t){var e;return null==(e=(u(t)?t.ownerDocument:t.document)||window.document)?void 0:e.documentElement}function u(t){return t instanceof Node||t instanceof s(t).Node}function a(t){return t instanceof Element||t instanceof s(t).Element}function d(t){return t instanceof HTMLElement||t instanceof s(t).HTMLElement}function h(t){return"undefined"!=typeof ShadowRoot&&(t instanceof ShadowRoot||t instanceof s(t).ShadowRoot)}function p(t){const{overflow:e,overflowX:n,overflowY:o,display:i}=x(t);return/auto|scroll|overlay|hidden|clip/.test(e+o+n)&&!["inline","contents"].includes(i)}function g(t){return["table","td","th"].includes(l(t))}function m(t){return[":popover-open",":modal"].some((e=>{try{return t.matches(e)}catch(t){return!1}}))}function y(t){const e=w(),n=a(t)?x(t):t;return"none"!==n.transform||"none"!==n.perspective||!!n.containerType&&"normal"!==n.containerType||!e&&!!n.backdropFilter&&"none"!==n.backdropFilter||!e&&!!n.filter&&"none"!==n.filter||["transform","perspective","filter"].some((t=>(n.willChange||"").includes(t)))||["paint","layout","strict","content"].some((t=>(n.contain||"").includes(t)))}function w(){return!("undefined"==typeof CSS||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}function v(t){return["html","body","#document"].includes(l(t))}function x(t){return s(t).getComputedStyle(t)}function b(t){return a(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.scrollX,scrollTop:t.scrollY}}function T(t){if("html"===l(t))return t;const e=t.assignedSlot||t.parentNode||h(t)&&t.host||f(t);return h(e)?e.host:e}function L(t){const e=T(t);return v(e)?t.ownerDocument?t.ownerDocument.body:t.body:d(e)&&p(e)?e:L(e)}function R(t,e,n){var o;void 0===e&&(e=[]),void 0===n&&(n=!0);const i=L(t),r=i===(null==(o=t.ownerDocument)?void 0:o.body),c=s(i);if(r){const t=C(c);return e.concat(c,c.visualViewport||[],p(i)?i:[],t&&n?R(t):[])}return e.concat(i,R(i,[],n))}function C(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function E(t){const e=x(t);let n=parseFloat(e.width)||0,o=parseFloat(e.height)||0;const r=d(t),c=r?t.offsetWidth:n,l=r?t.offsetHeight:o,s=i(n)!==c||i(o)!==l;return s&&(n=c,o=l),{width:n,height:o,$:s}}function S(t){return a(t)?t:t.contextElement}function F(t){const e=S(t);if(!d(e))return c(1);const n=e.getBoundingClientRect(),{width:o,height:r,$:l}=E(e);let s=(l?i(n.width):n.width)/o,f=(l?i(n.height):n.height)/r;return s&&Number.isFinite(s)||(s=1),f&&Number.isFinite(f)||(f=1),{x:s,y:f}}const O=c(0);function D(t){const e=s(t);return w()&&e.visualViewport?{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}:O}function H(t,n,o,i){void 0===n&&(n=!1),void 0===o&&(o=!1);const r=t.getBoundingClientRect(),l=S(t);let f=c(1);n&&(i?a(i)&&(f=F(i)):f=F(t));const u=function(t,e,n){return void 0===e&&(e=!1),!(!n||e&&n!==s(t))&&e}(l,o,i)?D(l):c(0);let d=(r.left+u.x)/f.x,h=(r.top+u.y)/f.y,p=r.width/f.x,g=r.height/f.y;if(l){const t=s(l),e=i&&a(i)?s(i):i;let n=t,o=C(n);for(;o&&i&&e!==n;){const t=F(o),e=o.getBoundingClientRect(),i=x(o),r=e.left+(o.clientLeft+parseFloat(i.paddingLeft))*t.x,c=e.top+(o.clientTop+parseFloat(i.paddingTop))*t.y;d*=t.x,h*=t.y,p*=t.x,g*=t.y,d+=r,h+=c,n=s(o),o=C(n)}}return e.rectToClientRect({width:p,height:g,x:d,y:h})}function P(t){return H(f(t)).left+b(t).scrollLeft}function W(t,n,i){let r;if("viewport"===n)r=function(t,e){const n=s(t),o=f(t),i=n.visualViewport;let r=o.clientWidth,c=o.clientHeight,l=0,u=0;if(i){r=i.width,c=i.height;const t=w();(!t||t&&"fixed"===e)&&(l=i.offsetLeft,u=i.offsetTop)}return{width:r,height:c,x:l,y:u}}(t,i);else if("document"===n)r=function(t){const e=f(t),n=b(t),i=t.ownerDocument.body,r=o(e.scrollWidth,e.clientWidth,i.scrollWidth,i.clientWidth),c=o(e.scrollHeight,e.clientHeight,i.scrollHeight,i.clientHeight);let l=-n.scrollLeft+P(t);const s=-n.scrollTop;return"rtl"===x(i).direction&&(l+=o(e.clientWidth,i.clientWidth)-r),{width:r,height:c,x:l,y:s}}(f(t));else if(a(n))r=function(t,e){const n=H(t,!0,"fixed"===e),o=n.top+t.clientTop,i=n.left+t.clientLeft,r=d(t)?F(t):c(1);return{width:t.clientWidth*r.x,height:t.clientHeight*r.y,x:i*r.x,y:o*r.y}}(n,i);else{const e=D(t);r={...n,x:n.x-e.x,y:n.y-e.y}}return e.rectToClientRect(r)}function M(t,e){const n=T(t);return!(n===e||!a(n)||v(n))&&("fixed"===x(n).position||M(n,e))}function z(t,e,n){const o=d(e),i=f(e),r="fixed"===n,s=H(t,!0,r,e);let u={scrollLeft:0,scrollTop:0};const a=c(0);if(o||!o&&!r)if(("body"!==l(e)||p(i))&&(u=b(e)),o){const t=H(e,!0,r,e);a.x=t.x+e.clientLeft,a.y=t.y+e.clientTop}else i&&(a.x=P(i));return{x:s.left+u.scrollLeft-a.x,y:s.top+u.scrollTop-a.y,width:s.width,height:s.height}}function A(t){return"static"===x(t).position}function V(t,e){return d(t)&&"fixed"!==x(t).position?e?e(t):t.offsetParent:null}function N(t,e){const n=s(t);if(m(t))return n;if(!d(t)){let e=T(t);for(;e&&!v(e);){if(a(e)&&!A(e))return e;e=T(e)}return n}let o=V(t,e);for(;o&&g(o)&&A(o);)o=V(o,e);return o&&v(o)&&A(o)&&!y(o)?n:o||function(t){let e=T(t);for(;d(e)&&!v(e);){if(y(e))return e;if(m(e))return null;e=T(e)}return null}(t)||n}const B={convertOffsetParentRelativeRectToViewportRelativeRect:function(t){let{elements:e,rect:n,offsetParent:o,strategy:i}=t;const r="fixed"===i,s=f(o),u=!!e&&m(e.floating);if(o===s||u&&r)return n;let a={scrollLeft:0,scrollTop:0},h=c(1);const g=c(0),y=d(o);if((y||!y&&!r)&&(("body"!==l(o)||p(s))&&(a=b(o)),d(o))){const t=H(o);h=F(o),g.x=t.x+o.clientLeft,g.y=t.y+o.clientTop}return{width:n.width*h.x,height:n.height*h.y,x:n.x*h.x-a.scrollLeft*h.x+g.x,y:n.y*h.y-a.scrollTop*h.y+g.y}},getDocumentElement:f,getClippingRect:function(t){let{element:e,boundary:i,rootBoundary:r,strategy:c}=t;const s=[..."clippingAncestors"===i?m(e)?[]:function(t,e){const n=e.get(t);if(n)return n;let o=R(t,[],!1).filter((t=>a(t)&&"body"!==l(t))),i=null;const r="fixed"===x(t).position;let c=r?T(t):t;for(;a(c)&&!v(c);){const e=x(c),n=y(c);n||"fixed"!==e.position||(i=null),(r?!n&&!i:!n&&"static"===e.position&&i&&["absolute","fixed"].includes(i.position)||p(c)&&!n&&M(t,c))?o=o.filter((t=>t!==c)):i=e,c=T(c)}return e.set(t,o),o}(e,this._c):[].concat(i),r],f=s[0],u=s.reduce(((t,i)=>{const r=W(e,i,c);return t.top=o(r.top,t.top),t.right=n(r.right,t.right),t.bottom=n(r.bottom,t.bottom),t.left=o(r.left,t.left),t}),W(e,f,c));return{width:u.right-u.left,height:u.bottom-u.top,x:u.left,y:u.top}},getOffsetParent:N,getElementRects:async function(t){const e=this.getOffsetParent||N,n=this.getDimensions,o=await n(t.floating);return{reference:z(t.reference,await e(t.floating),t.strategy),floating:{x:0,y:0,width:o.width,height:o.height}}},getClientRects:function(t){return Array.from(t.getClientRects())},getDimensions:function(t){const{width:e,height:n}=E(t);return{width:e,height:n}},getScale:F,isElement:a,isRTL:function(t){return"rtl"===x(t).direction}};const I=e.detectOverflow,k=e.offset,q=e.autoPlacement,U=e.shift,j=e.flip,X=e.size,Y=e.hide,$=e.arrow,_=e.inline,G=e.limitShift;t.arrow=$,t.autoPlacement=q,t.autoUpdate=function(t,e,i,c){void 0===c&&(c={});const{ancestorScroll:l=!0,ancestorResize:s=!0,elementResize:u="function"==typeof ResizeObserver,layoutShift:a="function"==typeof IntersectionObserver,animationFrame:d=!1}=c,h=S(t),p=l||s?[...h?R(h):[],...R(e)]:[];p.forEach((t=>{l&&t.addEventListener("scroll",i,{passive:!0}),s&&t.addEventListener("resize",i)}));const g=h&&a?function(t,e){let i,c=null;const l=f(t);function s(){var t;clearTimeout(i),null==(t=c)||t.disconnect(),c=null}return function f(u,a){void 0===u&&(u=!1),void 0===a&&(a=1),s();const{left:d,top:h,width:p,height:g}=t.getBoundingClientRect();if(u||e(),!p||!g)return;const m={rootMargin:-r(h)+"px "+-r(l.clientWidth-(d+p))+"px "+-r(l.clientHeight-(h+g))+"px "+-r(d)+"px",threshold:o(0,n(1,a))||1};let y=!0;function w(t){const e=t[0].intersectionRatio;if(e!==a){if(!y)return f();e?f(!1,e):i=setTimeout((()=>{f(!1,1e-7)}),1e3)}y=!1}try{c=new IntersectionObserver(w,{...m,root:l.ownerDocument})}catch(t){c=new IntersectionObserver(w,m)}c.observe(t)}(!0),s}(h,i):null;let m,y=-1,w=null;u&&(w=new ResizeObserver((t=>{let[n]=t;n&&n.target===h&&w&&(w.unobserve(e),cancelAnimationFrame(y),y=requestAnimationFrame((()=>{var t;null==(t=w)||t.observe(e)}))),i()})),h&&!d&&w.observe(h),w.observe(e));let v=d?H(t):null;return d&&function e(){const n=H(t);!v||n.x===v.x&&n.y===v.y&&n.width===v.width&&n.height===v.height||i();v=n,m=requestAnimationFrame(e)}(),i(),()=>{var t;p.forEach((t=>{l&&t.removeEventListener("scroll",i),s&&t.removeEventListener("resize",i)})),null==g||g(),null==(t=w)||t.disconnect(),w=null,d&&cancelAnimationFrame(m)}},t.computePosition=(t,n,o)=>{const i=new Map,r={platform:B,...o},c={...r.platform,_c:i};return e.computePosition(t,n,{...r,platform:c})},t.detectOverflow=I,t.flip=j,t.getOverflowAncestors=R,t.hide=Y,t.inline=_,t.limitShift=G,t.offset=k,t.platform=B,t.shift=U,t.size=X}));
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2ab9ab2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,18 @@
+{
+ "private": true,
+ "scripts": {
+ "lint": "npm -s run lint:js && npm -s run lint:i18n",
+ "lint:fix:js": "npm -s run lint:js -- --fix",
+ "lint:fix:styles": "npm -s run lint:styles -- --fix",
+ "lint:js": "eslint --cache --max-warnings 0 .",
+ "lint:styles": "stylelint \"**/*.{less,css}\"",
+ "lint:i18n": "banana-checker --requireLowerCase=0 i18n/"
+ },
+ "devDependencies": {
+ "eslint-config-wikimedia": "0.28.2",
+ "eslint-plugin-no-jquery": "3.0.2",
+ "grunt-banana-checker": "0.13.0",
+ "stylelint-config-idiomatic-order": "10.0.0",
+ "stylelint-config-wikimedia": "0.17.2"
+ }
+}
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..7347f11
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
+ "release-type": "php",
+ "pull-request-title-pattern": "build: release ${version}",
+ "extra-files": [
+ "extension.json",
+ "package.json",
+ "package-lock.json"
+ ],
+ "packages": {
+ ".": {}
+ }
+}