diff --git a/composer.json b/composer.json index 30169182527e49e5bdad4783101e7ac60811dcc2..0ba6b5842e386678fdfb0a95aeb984b905100b9f 100644 --- a/composer.json +++ b/composer.json @@ -126,7 +126,7 @@ "drupal/inline_entity_form": "1.0-rc15", "drupal/libraries": "4.0", "drupal/link_attributes": "1.12", - "drupal/linkit": "5.0-beta13", + "drupal/linkit": "6.0.0", "drupal/mathjax": "4.0.2", "drupal/media_entity_browser": "2.0-alpha4", "drupal/media_entity_file_replace": "^1.0", @@ -286,9 +286,6 @@ "drupal/honeypot": { "2811189": "https://www.drupal.org/files/issues/2022-05-25/honeypot-field_weight-2811189-27_0.patch" }, - "drupal/linkit": { - "2712951": "https://www.drupal.org/files/issues/2021-04-07/linkit-for-link-field-2712951-216.patch" - }, "drupal/social_media_links": { "Remove Google Plus": "patches/rm-googleplus.patch", "Fix Empty Link": "patches/accessibility-fix-empty-link.patch" diff --git a/composer.lock b/composer.lock index c99c2f904ce1c1a44095252d0203140485540258..f83787ef354ccc83a370354caaaf17cf807ff1f9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5e44a8642c1a018223a53f36ef6178a1", + "content-hash": "29697f1f20312fa7ad5e6e7db1e7e4e5", "packages": [ { "name": "alchemy/zippy", @@ -5098,32 +5098,36 @@ }, { "name": "drupal/linkit", - "version": "5.0.0-beta13", + "version": "6.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/linkit.git", - "reference": "8.x-5.0-beta13" + "reference": "6.0.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/linkit-8.x-5.0-beta13.zip", - "reference": "8.x-5.0-beta13", - "shasum": "9215fbea84166cabc9b7a2d9a04dedaffb9fc1ed" + "url": "https://ftp.drupal.org/files/projects/linkit-6.0.0.zip", + "reference": "6.0.0", + "shasum": "3c4143eb797abee04e5af47eb1885a65e6321b51" }, "require": { - "drupal/core": "^8.7.7 || ^9" + "drupal/core": "^9.4 || ^10.0.0" + }, + "conflict": { + "drupal/core": ">=10.1" }, "require-dev": { + "drupal/ckeditor": "*", "drupal/imce": "*" }, "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-5.0-beta13", - "datestamp": "1632946970", + "version": "6.0.0", + "datestamp": "1688748025", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, diff --git a/scripts/tmux-parallel-push.sh b/scripts/tmux-parallel-push.sh index df7193fb7e4a180cc165fb0803656f5778a0e985..ee661eab2bcd375134ab86b17ae982dbdb5e1baa 100755 --- a/scripts/tmux-parallel-push.sh +++ b/scripts/tmux-parallel-push.sh @@ -1,66 +1,79 @@ #!/usr/bin/env bash -if [[ -z `which parallel` || -z `which tmux` || -z `which gdate` ]]; then - echo "Either 'parallel' or 'tmux' is not installed..."; - if [[ -z `which brew` ]]; then - echo "Homebrew is NOT installed. Homebrew needs to be installed to proceed."; - echo "Visit https://https://brew.sh/ for more information."; - exit 1; +if [[ -z $(which parallel) || -z $(which tmux) || -z $(which gdate) ]]; then + echo "Either 'parallel' or 'tmux' is not installed..." + if [[ -z $(which brew) ]]; then + echo "Homebrew is NOT installed. Homebrew needs to be installed to proceed." + echo "Visit https://https://brew.sh/ for more information." + exit 1 else - echo "Attempting to install parallel, tmux, coreutils..."; - brew install parallel tmux coreutils; + echo "Attempting to install parallel, tmux, coreutils..." + brew install parallel tmux coreutils fi fi if [[ -z $1 ]]; then - echo; echo "Usage: $0 <env> [Deploy message]"; - echo; echo; - exit 1; + echo + echo "Usage: $0 <env> [Deploy message]" + echo + echo + exit 1 fi -ENV=$1; -shift; +ENV=$1 +shift if [[ $ENV == 'test' || $ENV == 'live' ]]; then if [[ -z $1 ]]; then while [[ -z $DEPLOY_MSG ]]; do - echo; - read -p "Enter deployment message: " DEPLOY_MSG; - echo; + echo + read -p "Enter deployment message: " DEPLOY_MSG + echo done else - DEPLOY_MSG=$@; + DEPLOY_MSG=$@ fi fi -NOW=`date +%Y%m%d_%H%M`; -LOG_DIR="deploy-$ENV-$NOW"; -mkdir $LOG_DIR; -echo "LOG_DIR: $LOG_DIR"; +NOW=$(date +%Y%m%d_%H%M) +LOG_DIR="deploy-$ENV-$NOW" +mkdir $LOG_DIR +echo "LOG_DIR: $LOG_DIR" -terminus org:site:list ohio-state-arts-and-sciences --upstream=5161e51a-0e4a-414c-974e-8565094b76b1 --tag=D8 --fields=name --format=string | sort | tee $LOG_DIR/site_list.txt; +terminus org:site:list ohio-state-arts-and-sciences --upstream=5161e51a-0e4a-414c-974e-8565094b76b1 --tag=D8 --fields=name --format=string | sort | tee $LOG_DIR/site_list.txt -parallel --delay 0.2 --tmuxpane --fg -j 36 -a $LOG_DIR/site_list.txt "scripts/deploy-site-env.sh {}.$ENV $DEPLOY_MSG 2>&1 | tee $LOG_DIR/{}.log"; +parallel --delay 0.2 -j 36 -a $LOG_DIR/site_list.txt "scripts/deploy-site-env.sh {}.$ENV $DEPLOY_MSG 2>&1 | tee $LOG_DIR/{}.log" -echo; echo; echo "Sleeping for 11 seconds..."; echo; echo; -sleep 11; +echo +echo +echo "Sleeping for 11 seconds..." +echo +echo +sleep 11 -reset; +reset # wait; -cd $LOG_DIR; -wc -l * | sort -n | tee line-counts.txt; +cd $LOG_DIR +wc -l * | sort -n | tee line-counts.txt -MERGE_ERR=$(grep -rnw './' -e "Merge conflict"); -if [[ -n ${MERGE_ERR} ]]; -then - echo; echo "It looks there were some errors. Review _merge-conflicts.txt for more information."; echo; - echo "$MERGE_ERR" > ./_merge-conflicts.txt; +MERGE_ERR=$(grep -rnw './' -e "Merge conflict") +if [[ -n ${MERGE_ERR} ]]; then + echo + echo "It looks there were some errors. Review _merge-conflicts.txt for more information." + echo + echo "$MERGE_ERR" >./_merge-conflicts.txt else - echo; echo "No automatic merge issues detected. Success?"; echo; + echo + echo "No automatic merge issues detected. Success?" + echo fi -cd ..; +cd .. -echo; echo; echo "Sleeping for 11 more seconds..."; echo; echo; -sleep 11; +echo +echo +echo "Sleeping for 11 more seconds..." +echo +echo +sleep 11 diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index abef8a072ab979df77d2ef22d43eaa3a1b3ebb82..20275aebac29b0da29ea3c83b81efa4ea271f584 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -5291,37 +5291,38 @@ }, { "name": "drupal/linkit", - "version": "5.0.0-beta13", - "version_normalized": "5.0.0.0-beta13", + "version": "6.0.0", + "version_normalized": "6.0.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/linkit.git", - "reference": "8.x-5.0-beta13" + "reference": "6.0.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/linkit-8.x-5.0-beta13.zip", - "reference": "8.x-5.0-beta13", - "shasum": "9215fbea84166cabc9b7a2d9a04dedaffb9fc1ed" + "url": "https://ftp.drupal.org/files/projects/linkit-6.0.0.zip", + "reference": "6.0.0", + "shasum": "3c4143eb797abee04e5af47eb1885a65e6321b51" }, "require": { - "drupal/core": "^8.7.7 || ^9" + "drupal/core": "^9.4 || ^10.0.0" + }, + "conflict": { + "drupal/core": ">=10.1" }, "require-dev": { + "drupal/ckeditor": "*", "drupal/imce": "*" }, "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-5.0-beta13", - "datestamp": "1632933670", + "version": "6.0.0", + "datestamp": "1688748025", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } - }, - "patches_applied": { - "2712951": "https://www.drupal.org/files/issues/2021-04-07/linkit-for-link-field-2712951-216.patch" } }, "installation-source": "dist", @@ -5339,6 +5340,10 @@ { "name": "johnwebdev", "homepage": "https://www.drupal.org/user/3331569" + }, + { + "name": "mark_fullmer", + "homepage": "https://www.drupal.org/user/2612816" } ], "description": "Linkit - Enriched linking experience", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index d8bae5bfa0ec8eac6bb42401e74118c2c297631a..e69ca4f69b11d927c8825258b596d2a63c5da29f 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'osu-asc-webservices/d8-upstream', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '4cbb2152350103f297f5852d6a6816b907061d82', + 'reference' => 'eedf8cf2fe6599bb32dea9e590be4b8a93b63825', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -887,9 +887,9 @@ 'dev_requirement' => false, ), 'drupal/linkit' => array( - 'pretty_version' => '5.0.0-beta13', - 'version' => '5.0.0.0-beta13', - 'reference' => '8.x-5.0-beta13', + 'pretty_version' => '6.0.0', + 'version' => '6.0.0.0', + 'reference' => '6.0.0', 'type' => 'drupal-module', 'install_path' => __DIR__ . '/../../web/modules/linkit', 'aliases' => array(), @@ -1549,7 +1549,7 @@ 'osu-asc-webservices/d8-upstream' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '4cbb2152350103f297f5852d6a6816b907061d82', + 'reference' => 'eedf8cf2fe6599bb32dea9e590be4b8a93b63825', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/web/modules/linkit/.tugboat/config.yml b/web/modules/linkit/.tugboat/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..9e8c4bb26ff05d9aad3a02271af1bac49a7c6836 --- /dev/null +++ b/web/modules/linkit/.tugboat/config.yml @@ -0,0 +1,52 @@ +# @see: https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupal/using-live-previews-on-drupal-core-and-contrib-merge-requests#s-adding-live-previews-to-a-contributed-module +services: + php: + image: q0rban/tugboat-drupal:10 + default: true + http: false + depends: mysql + commands: + update: | + set -eux + # Check out a branch using the unique Tugboat ID for this repository, to + # ensure we don't clobber an existing branch. + git checkout -b $TUGBOAT_REPO_ID + # Composer is hungry. You need a Tugboat project with a pretty sizeable + # chunk of memory. + export COMPOSER_MEMORY_LIMIT=-1 + # This is an environment variable we added in the Dockerfile that + # provides the path to Drupal composer root (not the web root). + cd $DRUPAL_COMPOSER_ROOT + # We configure the Drupal project to use the checkout of the module as a + # Composer package repository. + composer config repositories.tugboat vcs $TUGBOAT_ROOT + # Now we can require this module, specifing the branch name we created + # above that uses the $TUGBOAT_REPO_ID environment variable. + composer require drupal/linkit:dev-$TUGBOAT_REPO_ID + # Install Drupal on the site. + vendor/bin/drush \ + --yes \ + --db-url=mysql://tugboat:tugboat@mysql:3306/tugboat \ + --site-name="Live preview for ${TUGBOAT_PREVIEW_NAME}" \ + --account-pass=admin \ + site:install standard + # Set up the files directory permissions. + mkdir -p $DRUPAL_DOCROOT/sites/default/files + chgrp -R www-data $DRUPAL_DOCROOT/sites/default/files + chmod 2775 $DRUPAL_DOCROOT/sites/default/files + chmod -R g+w $DRUPAL_DOCROOT/sites/default/files + # Enable the module. + vendor/bin/drush --yes pm:enable linkit + build: | + set -eux + # Delete and re-check out this branch in case this is built from a Base Preview. + git branch -D $TUGBOAT_REPO_ID && git checkout -b $TUGBOAT_REPO_ID || true + export COMPOSER_MEMORY_LIMIT=-1 + cd $DRUPAL_COMPOSER_ROOT + composer install --optimize-autoloader + # Update this module, including all dependencies. + composer update drupal/linkit --with-all-dependencies + vendor/bin/drush --yes updb + vendor/bin/drush cache:rebuild + mysql: + image: tugboatqa/mariadb diff --git a/web/modules/linkit/PATCHES.txt b/web/modules/linkit/PATCHES.txt deleted file mode 100644 index ad45756bcc49243ecb529af58a6e4b22ca371ea8..0000000000000000000000000000000000000000 --- a/web/modules/linkit/PATCHES.txt +++ /dev/null @@ -1,7 +0,0 @@ -This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) -Patches applied to this directory: - -2712951 -Source: https://www.drupal.org/files/issues/2021-04-07/linkit-for-link-field-2712951-216.patch - - diff --git a/web/modules/linkit/README.md b/web/modules/linkit/README.md index b4ecac50c56cccc2c6b58195b8a1aa7df7c2c392..fb547c87a499a09a3d49da2cc49d66b65dd616b6 100644 --- a/web/modules/linkit/README.md +++ b/web/modules/linkit/README.md @@ -1,9 +1,9 @@ INTRODUCTION ------------ -Linkit provides an **enriched linking experience for internal and -external linking** with editors by using an autocomplete field. Linkit -has by default support for nodes, users, taxonomy terms, files, -comments and **basic support for all types of entities** that defines a +Linkit provides an **enriched linking experience for internal and +external linking** with editors by using an autocomplete field. Linkit +has by default support for nodes, users, taxonomy terms, files, +comments and **basic support for all types of entities** that defines a canonical link template. * For a full description of the module, visit the project page: @@ -12,6 +12,23 @@ canonical link template. * To submit bug reports and feature suggestions, or track changes: https://www.drupal.org/project/issues/linkit + +DRUPAL CORE FEATURE COMPARISON +------------------------------ +Drupal core will provide link autocomplete suggestions in CKEditor. See +https://www.drupal.org/project/drupal/issues/3317769 + +Below is a list of features included in Linkit which will not initially be in +Drupal core. + + * Configurable autocomplete on link fields. + * Ability to configure metadata (with token support) for autcomplete suggestions + * Ability to control number of items shown + * Ability to toggle published/unpublished entities in suggestions + * IMCE integration + * Pluggable, configurable, and alterable matchers + * Pluggable, configurable, and alterable attributes + REQUIREMENTS ------------ @@ -25,16 +42,16 @@ INSTALLATION https://www.drupal.org/documentation/install/modules-themes/modules-8 * **Enable Linkit** -To enable Linkit, go to `/admin/config/content/formats` and edit the -desired text format you want to enable Linkit for. Linkit will alter -the default link plugin, so make sure that it is enabled. When the -default link plugin is enabled, you will have to select a Linkit +To enable Linkit, go to `/admin/config/content/formats` and edit the +desired text format you want to enable Linkit for. Linkit will alter +the default link plugin, so make sure that it is enabled. When the +default link plugin is enabled, you will have to select a Linkit profile to use in the "Drupal link" tab under the toolbar configuration. * **Enable Linkit filter** -Linkit will insert URLs in a format like "entity:node/1". The Linkit -filter will then transform that URL into a "real" URL when rendering -the text. **Note: The Linkit filter must run before "Limit allowed HTML +Linkit will insert URLs in a format like "entity:node/1". The Linkit +filter will then transform that URL into a "real" URL when rendering +the text. **Note: The Linkit filter must run before "Limit allowed HTML tags and correct faulty HTML"**. * If the **Limit allowed HTML tags and correct faulty HTML** filter is @@ -51,8 +68,8 @@ If automatic titles is enabled in the Linkit filter settings, and CONFIGURATION ------------- -A default Linkit profile will have been installed as a step in the -module installation process. The profile will contain information about +A default Linkit profile will have been installed as a step in the +module installation process. The profile will contain information about which plugins to use. You can create additional profiles at `/admin/config/content/linkit`. @@ -61,7 +78,7 @@ You can create additional profiles at `/admin/config/content/linkit`. PLUGIN EXAMPLES --------------- -There are plugin implementation examples in the linkit_test module +There are plugin implementation examples in the linkit_test module bundled with Linkit core. @@ -70,3 +87,4 @@ MAINTAINERS Current maintainers: * Emil Stjerneman (anon) - https://www.drupal.org/user/464598 + * Mark Fullmer (mark_fullmer) - https://www.drupal.org/u/mark_fullmer diff --git a/web/modules/linkit/composer.json b/web/modules/linkit/composer.json index eb28ea2f63458db99b4096d3f0dde4945fb554dd..0c178d7949d88290806881883ed09f1c86f58604 100644 --- a/web/modules/linkit/composer.json +++ b/web/modules/linkit/composer.json @@ -16,7 +16,10 @@ "source": "http://cgit.drupalcode.org/linkit" }, "require" : { - "drupal/core": "^8.7.7 || ^9" + "drupal/core": "^9.4 || ^10.0.0" + }, + "conflict": { + "drupal/core": ">=10.1" }, "license": "GPL-2.0-or-later" } diff --git a/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml b/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml index 69792fe03b19012bf69eaea471c29f786baf353f..1d6d88ea43991f398409d7139cf8c9728a7bc635 100644 --- a/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml +++ b/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml @@ -11,3 +11,4 @@ effects: data: width: 50 height: 50 + anchor: center-center diff --git a/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml b/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml index 7aa4092fa8399d94412db01caadba9365a19faee..6708c63490ce440f8dd6c6edc5d3e7df0cf69318 100644 --- a/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml +++ b/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml @@ -12,8 +12,9 @@ matchers: id: 'entity:node' weight: 0 settings: - metadata: 'by [node:author] | [node:created:medium]' + metadata: '[node:content-type:name] #[node:nid] | [node:created:medium] by [node:author]' bundles: { } group_by_bundle: false include_unpublished: false substitution_type: canonical + limit: 100 diff --git a/web/modules/linkit/config/schema/linkit.schema.yml b/web/modules/linkit/config/schema/linkit.schema.yml index 3787e95c9abca4a8bcac582ee501287bca102c0f..f9eef21c4b3dd957c9b5c19be1f0500e61862a2d 100644 --- a/web/modules/linkit/config/schema/linkit.schema.yml +++ b/web/modules/linkit/config/schema/linkit.schema.yml @@ -133,3 +133,19 @@ field.formatter.settings.linkit: linkit_profile: type: string label: 'Linkit profile' + +ckeditor5.plugin.linkit_extension: + type: mapping + label: Linkit + constraints: + Callback: [\Drupal\linkit\Plugin\CKEditor5Plugin\Linkit, requireProfileIfEnabled] + mapping: + linkit_enabled: + type: boolean + label: 'Use Linkit' + linkit_profile: + type: string + label: 'Linkit profile' + constraints: + Choice: + callback: \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit::validChoices diff --git a/web/modules/linkit/css/linkit.autocomplete.css b/web/modules/linkit/css/linkit.autocomplete.css index 4159a495f94a7967384c64bee1f9784c370c4aec..2476f91d3a14b4f1f474a95ee403cf2f433991e2 100644 --- a/web/modules/linkit/css/linkit.autocomplete.css +++ b/web/modules/linkit/css/linkit.autocomplete.css @@ -25,10 +25,13 @@ .linkit-ui-autocomplete { max-height: calc((100vh - 80px)/2); overflow: auto; + position: relative; } .linkit-ui-autocomplete.ui-widget { font-size: .9em; + max-width: inherit; + position: absolute; } .linkit-ui-autocomplete.ui-menu .linkit-result-line-wrapper { @@ -44,6 +47,11 @@ color: #fff; } +.ui-autocomplete .linkit-result-line-wrapper.ui-menu-item-wrapper.ui-state-active, +.ui-autocomplete .linkit-result-line-wrapper.ui-menu-item-wrapper.ui-state-focus { + background: #bfbfbf; +} + .linkit-result-line:not(:last-of-type) { border-bottom: 1px solid #bfbfbf; } diff --git a/web/modules/linkit/js/build/linkit.js b/web/modules/linkit/js/build/linkit.js new file mode 100644 index 0000000000000000000000000000000000000000..d45e048b744171a0763519f333c17f6c64eda08e --- /dev/null +++ b/web/modules/linkit/js/build/linkit.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.linkit=e())}(self,(()=>(()=>{var t={"ckeditor5/src/core.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/typing.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/typing.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(n){var o=e[n];if(void 0!==o)return o.exports;var s=e[n]={exports:{}};return t[n](s,s.exports,i),s.exports}i.d=(t,e)=>{for(var n in e)i.o(e,n)&&!i.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var n={};return(()=>{"use strict";i.d(n,{default:()=>u});var t=i("ckeditor5/src/core.js"),e=i("ckeditor5/src/typing.js");class o extends t.Plugin{init(){this.attrs=["data-entity-type","data-entity-uuid","data-entity-substitution"],this._allowAndConvertExtraAttributes(),this._removeExtraAttributesOnUnlinkCommandExecute(),this._refreshExtraAttributeValues(),this._addExtraAttributesOnLinkCommandExecute()}_allowAndConvertExtraAttributes(){const t=this.editor;t.model.schema.extend("$text",{allowAttributes:this.attrs}),this.attrs.forEach((e=>{t.conversion.for("downcast").attributeToElement({model:e,view:(t,{writer:i})=>{const n=i.createAttributeElement("a",{[e]:t},{priority:5});return i.setCustomProperty("link",!0,n),n}}),t.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{[e]:!0}},model:{key:e,value:t=>t.getAttribute(e)}})}))}_addExtraAttributesOnLinkCommandExecute(){const t=this.editor,e=t.commands.get("link");let i=!1;e.on("execute",((e,n)=>{if(n.length<3)return;if(i)return void(i=!1);e.stop(),i=!0;const o=n[n.length-1],s=this.editor.model,r=s.document.selection;s.change((e=>{t.execute("link",...n);const i=r.getFirstPosition();this.attrs.forEach((t=>{if(r.isCollapsed){const n=i.textNode||i.nodeBefore;o[t]?e.setAttribute(t,o[t],e.createRangeOn(n)):e.removeAttribute(t,e.createRangeOn(n)),e.removeSelectionAttribute(t)}else{const i=s.schema.getValidRanges(r.getRanges(),t);for(const n of i)o[t]?e.setAttribute(t,o[t],n):e.removeAttribute(t,n)}}))}))}),{priority:"high"})}_removeExtraAttributesOnUnlinkCommandExecute(){const t=this.editor,i=t.commands.get("unlink"),n=this.editor.model,o=n.document.selection;let s=!1;i.on("execute",(i=>{s||(i.stop(),n.change((()=>{s=!0,t.execute("unlink"),s=!1,n.change((t=>{let i;this.attrs.forEach((s=>{i=o.isCollapsed?[(0,e.findAttributeRange)(o.getFirstPosition(),s,o.getAttribute(s),n)]:n.schema.getValidRanges(o.getRanges(),s);for(const e of i)t.removeAttribute(s,e)}))}))})))}),{priority:"high"})}_refreshExtraAttributeValues(){const t=this.editor,e=this.attrs,i=t.commands.get("link"),n=this.editor.model,o=n.document.selection;e.forEach((t=>{i.set(t,null)})),n.document.on("change",(()=>{e.forEach((t=>{i[t]=o.getAttribute(t)}))}))}static get pluginName(){return"LinkitEditing"}}const s=jQuery;function r(t,e){var i=s("<li>").addClass("linkit-result-line"),n=s("<div>").addClass("linkit-result-line-wrapper");return n.append(s("<span>").html(e.label).addClass("linkit-result-line--title")),e.hasOwnProperty("description")&&n.append(s("<span>").html(e.description).addClass("linkit-result-line--description")),i.append(n).appendTo(t)}function a(t,e){var i=this.element.autocomplete("instance"),n={};e.forEach((function(t){const e=t.hasOwnProperty("group")?t.group:"";n.hasOwnProperty(e)||(n[e]=[]),n[e].push(t)})),s.each(n,(function(e,n){e.length&&t.append('<li class="linkit-result-line--group ui-menu-divider">'+e+"</li>"),s.each(n,(function(e,n){i._renderItemData(t,n)}))}))}class l extends t.Plugin{static get requires(){return[o]}init(){this._state={};this.editor.config.get("linkit");this._enableLinkAutocomplete(),this._handleExtraFormFieldSubmit(),this._handleDataLoadingIntoExtraFormField()}_enableLinkAutocomplete(){const t=this.editor,e=t.config.get("linkit"),i=t.plugins.get("LinkUI").formView;let n=!1;i.extendTemplate({attributes:{class:["ck-vertical-form","ck-link-form_layout-vertical"]}}),t.plugins.get("ContextualBalloon")._rotatorView.content.on("add",((t,o)=>{if(o!==i||n)return;let l;!function(t,e){const{autocompleteUrl:i,selectHandler:n,closeHandler:o,openHandler:l}=e,u={cache:{},ajax:{dataType:"json",jsonp:!1}},d={appendTo:t.closest(".ck-labeled-field-view"),source:function(t,e){const{cache:n}=u;var o=t.term;n.hasOwnProperty(o)?e(n[o]):s.ajax(i,{success:function(t){n[o]=t.suggestions,e(t.suggestions)},data:{q:o},...u.ajax})},select:n,focus:()=>!1,search:()=>!d.isComposing,close:o,open:l,minLength:1,isComposing:!1},c=s(t).autocomplete(d),p=c.data("ui-autocomplete");p.widget().menu("option","items","> :not(.linkit-result-line--group)"),p._renderMenu=a,p._renderItem=r,c.autocomplete("widget").addClass("linkit-ui-autocomplete ck-reset_all-excluded"),c.on("click",(function(){c.autocomplete("search",c.val())})),c.on("compositionstart.autocomplete",(function(){d.isComposing=!0})),c.on("compositionend.autocomplete",(function(){d.isComposing=!1}))}(i.urlInputView.fieldView.element,{...e,selectHandler:(t,{item:e})=>{if(!e.path)throw"Missing path param."+JSON.stringify(e);if(e.entity_type_id||e.entity_uuid||e.substitution_id){if(!e.entity_type_id||!e.entity_uuid||!e.substitution_id)throw"Missing path param."+JSON.stringify(e);this.set("entityType",e.entity_type_id),this.set("entityUuid",e.entity_uuid),this.set("entitySubstitution",e.substitution_id)}else this.set("entityType",null),this.set("entityUuid",null),this.set("entitySubstitution",null);return t.target.value=e.path,l=!0,!1},openHandler:t=>{l=!1},closeHandler:t=>{l||(this.set("entityType",null),this.set("entityUuid",null),this.set("entitySubstitution",null)),l=!1}}),n=!0,i.urlInputView.fieldView.template.attributes.class.push("form-linkit-autocomplete")}))}_handleExtraFormFieldSubmit(){const t=this.editor,e=t.plugins.get("LinkUI").formView,i=t.commands.get("link");this.listenTo(e,"submit",(()=>{const t={"data-entity-type":this.entityType,"data-entity-uuid":this.entityUuid,"data-entity-substitution":this.entitySubstitution};i.once("execute",((e,i)=>{if(i.length<3)i.push(t);else{if(3!==i.length)throw Error("The link command has more than 3 arguments.");Object.assign(i[2],t)}}),{priority:"highest"})}),{priority:"high"})}_handleDataLoadingIntoExtraFormField(){const t=this.editor.commands.get("link");this.bind("entityType").to(t,"data-entity-type"),this.bind("entityUuid").to(t,"data-entity-uuid"),this.bind("entitySubstitution").to(t,"data-entity-substitution")}static get pluginName(){return"Linkit"}}const u={Linkit:l}})(),n=n.default})())); \ No newline at end of file diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/README.md b/web/modules/linkit/js/ckeditor5_plugins/linkit/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e6d004a540bb7450b34d433cdc3efb226c5e573d --- /dev/null +++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/README.md @@ -0,0 +1,9 @@ +This plugin is largely based on CKEditor 5's [block plugin widget tutorial](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/tutorials/implementing-a-block-widget.html), +but with added documentation to facilitate better understanding of CKEditor 5 +plugin development and other minor changes. + +Within `/src` are the multiple files that will be used by the build process to +become a CKEditor 5 plugin in `/build`. Technically, everything in these files +could be in a single `index.js` - the only file the MUST be present is +`/src/index.js`. However, splitting the plugin into concern-specific files has +maintainability benefits. diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/src/autocomplete.js b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/autocomplete.js new file mode 100644 index 0000000000000000000000000000000000000000..be9d908bebabc2e0e712fda941cb6e2f4697056f --- /dev/null +++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/autocomplete.js @@ -0,0 +1,137 @@ +const $ = jQuery; + + +/** + * Override jQuery UI _renderItem function to output HTML by default. + * + * @param {object} ul + * The <ul> element that the newly created <li> element must be appended to. + * @param {object} item + * The list item to append. + * + * @return {object} + * jQuery collection of the ul element. + */ +function renderItem(ul, item) { + var $line = $('<li>').addClass('linkit-result-line'); + var $wrapper = $('<div>').addClass('linkit-result-line-wrapper'); + $wrapper.append($('<span>').html(item.label).addClass('linkit-result-line--title')); + + if (item.hasOwnProperty('description')) { + $wrapper.append($('<span>').html(item.description).addClass('linkit-result-line--description')); + } + return $line.append($wrapper).appendTo(ul); +} + +/** + * Override jQuery UI _renderMenu function to handle groups. + * + * @param {object} ul + * An empty <ul> element to use as the widget's menu. + * @param {array} items + * An Array of items that match the user typed term. + */ +function renderMenu(ul, items) { + var self = this.element.autocomplete('instance'); + + var grouped_items = {}; + items.forEach(function (item) { + const group = item.hasOwnProperty('group') ? item.group : ''; + if (!grouped_items.hasOwnProperty(group)) { + grouped_items[group] = []; + } + grouped_items[group].push(item); + }); + + $.each(grouped_items, function (group, items) { + if (group.length) { + ul.append('<li class="linkit-result-line--group ui-menu-divider">' + group + '</li>'); + } + + $.each(items, function (index, item) { + self._renderItemData(ul, item); + }); + }); +} + +export default function initializeAutocomplete(element, settings) { + const { autocompleteUrl, selectHandler, closeHandler, openHandler } = settings; + const autocomplete = { + cache: {}, + ajax: { + dataType: 'json', + jsonp: false, + }, + }; + + /** + * JQuery UI autocomplete source callback. + * + * @param {object} request + * The request object. + * @param {function} response + * The function to call with the response. + */ + function sourceData(request, response) { + const { cache } = autocomplete; + /** + * Transforms the data object into an array and update autocomplete results. + * + * @param {object} data + * The data sent back from the server. + */ + function sourceCallbackHandler(data) { + cache[term] = data.suggestions; + response(data.suggestions); + } + + // Get the desired term and construct the autocomplete URL for it. + var term = request.term; + + // Check if the term is already cached. + if (cache.hasOwnProperty(term)) { + response(cache[term]); + } + else { + $.ajax(autocompleteUrl, { + success: sourceCallbackHandler, + data: {q: term}, + ...autocomplete.ajax, + }); + } + } + + const options = { + appendTo: element.closest('.ck-labeled-field-view'), + source: sourceData, + select: selectHandler, + focus: () => false, + search: () => !options.isComposing, + close: closeHandler, + open: openHandler, + minLength: 1, + isComposing: false, + } + const $auto = $(element).autocomplete(options); + + // Override a few things. + const instance = $auto.data('ui-autocomplete'); + instance.widget().menu('option', 'items', '> :not(.linkit-result-line--group)'); + instance._renderMenu = renderMenu; + instance._renderItem = renderItem; + + $auto.autocomplete('widget').addClass('linkit-ui-autocomplete ck-reset_all-excluded'); + + $auto.on('click', function () { + $auto.autocomplete('search', $auto.val()); + }); + + $auto.on('compositionstart.autocomplete', function () { + options.isComposing = true; + }); + $auto.on('compositionend.autocomplete', function () { + options.isComposing = false; + }); + + return $auto; +} diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/src/index.js b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7a33ccbeed7729c9c392f609f72064026e8e804b --- /dev/null +++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/index.js @@ -0,0 +1,143 @@ +import { Plugin } from 'ckeditor5/src/core'; +import LinkitEditing from './linkitediting'; +import initializeAutocomplete from './autocomplete'; + +class Linkit extends Plugin { + /** + * @inheritdoc + */ + static get requires() { + return [LinkitEditing]; + } + + init() { + this._state = {}; + const editor = this.editor; + const options = editor.config.get('linkit'); + this._enableLinkAutocomplete(); + this._handleExtraFormFieldSubmit(); + this._handleDataLoadingIntoExtraFormField(); + } + + _enableLinkAutocomplete() { + const editor = this.editor; + const options = editor.config.get('linkit'); + const linkFormView = editor.plugins.get( 'LinkUI' ).formView; + let wasAutocompleteAdded = false; + + linkFormView.extendTemplate( { + attributes: { + class: ['ck-vertical-form', 'ck-link-form_layout-vertical'] + } + } ); + + editor.plugins.get( 'ContextualBalloon' )._rotatorView.content.on('add', ( evt, view ) => { + if ( view !== linkFormView || wasAutocompleteAdded ) { + return; + } + + /** + * Used to know if a selection was made from the autocomplete results. + * + * @type {boolean} + */ + let selected; + + initializeAutocomplete( + linkFormView.urlInputView.fieldView.element, + { + ...options, + selectHandler: (event, { item }) => { + if (!item.path) { + throw 'Missing path param.' + JSON.stringify(item); + } + + if (item.entity_type_id || item.entity_uuid || item.substitution_id) { + if (!item.entity_type_id || !item.entity_uuid || !item.substitution_id) { + throw 'Missing path param.' + JSON.stringify(item); + } + + this.set('entityType', item.entity_type_id); + this.set('entityUuid', item.entity_uuid); + this.set('entitySubstitution', item.substitution_id); + } + else { + this.set('entityType', null); + this.set('entityUuid', null); + this.set('entitySubstitution', null); + } + + event.target.value = item.path; + selected = true; + return false; + }, + openHandler: (event) => { + selected = false; + }, + closeHandler: (event) => { + if (!selected) { + this.set('entityType', null); + this.set('entityUuid', null); + this.set('entitySubstitution', null); + } + selected = false; + }, + }, + ); + + wasAutocompleteAdded = true; + linkFormView.urlInputView.fieldView.template.attributes.class.push('form-linkit-autocomplete'); + }); + } + + _handleExtraFormFieldSubmit() { + const editor = this.editor; + const linkFormView = editor.plugins.get('LinkUI').formView; + const linkCommand = editor.commands.get('link'); + + this.listenTo(linkFormView, 'submit', () => { + const values = { + 'data-entity-type': this.entityType, + 'data-entity-uuid': this.entityUuid, + 'data-entity-substitution': this.entitySubstitution, + } + // Stop the execution of the link command caused by closing the form. + // Inject the extra attribute value. The highest priority listener here + // injects the argument (here below 👇). + // - The high priority listener in + // _addExtraAttributeOnLinkCommandExecute() gets that argument and sets + // the extra attribute. + // - The normal (default) priority listener in ckeditor5-link sets + // (creates) the actual link. + linkCommand.once('execute', (evt, args) => { + if (args.length < 3) { + args.push(values); + } else if (args.length === 3) { + Object.assign(args[2], values); + } else { + throw Error('The link command has more than 3 arguments.') + } + }, { priority: 'highest' }); + }, { priority: 'high' }); + } + + _handleDataLoadingIntoExtraFormField() { + const editor = this.editor; + const linkCommand = editor.commands.get('link'); + + this.bind('entityType').to(linkCommand, 'data-entity-type'); + this.bind('entityUuid').to(linkCommand, 'data-entity-uuid'); + this.bind('entitySubstitution').to(linkCommand, 'data-entity-substitution'); + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'Linkit'; + } +} + +export default { + Linkit, +}; diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/src/linkitediting.js b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/linkitediting.js new file mode 100644 index 0000000000000000000000000000000000000000..9e1737621d8c6015649312c9e4a548feea865e7d --- /dev/null +++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/linkitediting.js @@ -0,0 +1,191 @@ +import { Plugin } from 'ckeditor5/src/core'; +import { findAttributeRange } from 'ckeditor5/src/typing'; + +export default class LinkitEditing extends Plugin { + init() { + this.attrs = ['data-entity-type', 'data-entity-uuid', 'data-entity-substitution']; + this._allowAndConvertExtraAttributes(); + this._removeExtraAttributesOnUnlinkCommandExecute(); + this._refreshExtraAttributeValues(); + this._addExtraAttributesOnLinkCommandExecute(); + } + + _allowAndConvertExtraAttributes() { + const editor = this.editor; + + editor.model.schema.extend('$text', { allowAttributes: this.attrs }); + + // Model -> View (DOM) + this.attrs.forEach((attribute) => { + editor.conversion.for('downcast').attributeToElement({ + model: attribute, + view: (value, { writer }) => { + const linkViewElement = writer.createAttributeElement('a', { + [attribute]: value + }, { priority: 5 }); + + // Without it the isLinkElement() will not recognize the link and the UI will not show up + // when the user clicks a link. + writer.setCustomProperty('link', true, linkViewElement); + + return linkViewElement; + } + }); + + // View (DOM/DATA) -> Model + editor.conversion.for('upcast') + .elementToAttribute({ + view: { + name: 'a', + attributes: { + [attribute]: true, + } + }, + model: { + key: attribute, + value: viewElement => viewElement.getAttribute(attribute), + } + }); + }); + } + + _addExtraAttributesOnLinkCommandExecute() { + const editor = this.editor; + const linkCommand = editor.commands.get('link'); + let linkCommandExecuting = false; + + linkCommand.on('execute', (evt, args) => { + // Custom handling is only required if an extra attribute was passed into + // editor.execute( 'link', ... ). + if (args.length < 3) { + return; + } + if (linkCommandExecuting) { + linkCommandExecuting = false; + return; + } + + // If the additional attribute was passed, we stop the default execution + // of the LinkCommand. We're going to create Model#change() block for undo + // and execute the LinkCommand together with setting the extra attribute. + evt.stop(); + + // Prevent infinite recursion by keeping records of when link command is + // being executed by this function. + linkCommandExecuting = true; + const extraAttributeValues = args[args.length - 1]; + const model = this.editor.model; + const selection = model.document.selection; + + // Wrapping the original command execution in a model.change() block to + // make sure there's a single undo step when the extra attribute is added. + model.change((writer) => { + editor.execute('link', ...args); + + const firstPosition = selection.getFirstPosition(); + + this.attrs.forEach((attribute) => { + if (selection.isCollapsed) { + const node = firstPosition.textNode || firstPosition.nodeBefore; + + if (extraAttributeValues[attribute]) { + writer.setAttribute(attribute, extraAttributeValues[attribute], writer.createRangeOn(node)); + } else { + writer.removeAttribute(attribute, writer.createRangeOn(node)); + } + + writer.removeSelectionAttribute(attribute); + } else { + const ranges = model.schema.getValidRanges(selection.getRanges(), attribute); + + for (const range of ranges) { + if (extraAttributeValues[attribute]) { + writer.setAttribute(attribute, extraAttributeValues[attribute], range); + } else { + writer.removeAttribute(attribute, range); + } + } + } + }); + }); + }, { priority: 'high' } ); + } + + _removeExtraAttributesOnUnlinkCommandExecute() { + const editor = this.editor; + const unlinkCommand = editor.commands.get('unlink'); + const model = this.editor.model; + const selection = model.document.selection; + + let isUnlinkingInProgress = false; + + // Make sure all changes are in a single undo step so cancel the original unlink first in the high priority. + unlinkCommand.on('execute', evt => { + if (isUnlinkingInProgress) { + return; + } + + evt.stop(); + + // This single block wraps all changes that should be in a single undo step. + model.change(() => { + // Now, in this single "undo block" let the unlink command flow naturally. + isUnlinkingInProgress = true; + + // Do the unlinking within a single undo step. + editor.execute('unlink'); + + // Let's make sure the next unlinking will also be handled. + isUnlinkingInProgress = false; + + // The actual integration that removes the extra attribute. + model.change(writer => { + // Get ranges to unlink. + let ranges; + + this.attrs.forEach((attribute) => { + if (selection.isCollapsed) { + ranges = [findAttributeRange( + selection.getFirstPosition(), + attribute, + selection.getAttribute(attribute), + model + )]; + } else { + ranges = model.schema.getValidRanges(selection.getRanges(), attribute); + } + + // Remove the extra attribute from specified ranges. + for (const range of ranges) { + writer.removeAttribute(attribute, range); + } + }); + }); + }); + }, { priority: 'high' }); + } + + _refreshExtraAttributeValues() { + const editor = this.editor; + const attributes = this.attrs + const linkCommand = editor.commands.get('link'); + const model = this.editor.model; + const selection = model.document.selection; + + attributes.forEach((attribute) => { + linkCommand.set(attribute, null); + }); + model.document.on('change', () => { + attributes.forEach((attribute) => { + linkCommand[attribute] = selection.getAttribute(attribute); + }); + }); + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'LinkitEditing'; + } +} diff --git a/web/modules/linkit/js/linkit.autocomplete.js b/web/modules/linkit/js/linkit.autocomplete.js index 8ef722fe6000569b1f114281f85fa40064248cb5..594a2d10e00c61c83e247c65f3839a5d68ffc440 100644 --- a/web/modules/linkit/js/linkit.autocomplete.js +++ b/web/modules/linkit/js/linkit.autocomplete.js @@ -3,7 +3,7 @@ * Linkit Autocomplete based on jQuery UI. */ -(function ($, Drupal, _) { +(function ($, Drupal, once) { 'use strict'; @@ -63,6 +63,7 @@ * False to prevent further handlers. */ function selectHandler(event, ui) { + var linkSelector = event.target.getAttribute('data-drupal-selector'); var $context = $(event.target).closest('form,fieldset,tr'); if (!ui.item.path) { @@ -83,12 +84,18 @@ if (ui.item.label) { // Automatically set the link title. - var $linkTitle = $('.linkit-widget-title--autofill-enabled', $context); + var $linkTitle = $('*[data-linkit-widget-title-autofill-enabled]', $context); if ($linkTitle.length > 0) { + var titleSelector = $linkTitle.attr('data-drupal-selector'); + if (titleSelector === undefined || linkSelector === undefined) { + return false; + } + if (titleSelector.replace('-title', '') !== linkSelector.replace('-uri', '')) { + return false; + } if (!$linkTitle.val() || $linkTitle.hasClass('link-widget-title--auto')) { // Set value to the label. $linkTitle.val(ui.item.label); - // Flag title as being automatically set. $linkTitle.addClass('link-widget-title--auto'); } @@ -133,9 +140,14 @@ function renderMenu(ul, items) { var self = this.element.autocomplete('instance'); - var grouped_items = _.groupBy(items, function (item) { - return item.hasOwnProperty('group') ? item.group : ''; - }); + var grouped_items = {}; + items.forEach(function (item) { + const group = item.hasOwnProperty('group') ? item.group : ''; + if (!grouped_items.hasOwnProperty(group)) { + grouped_items[group] = []; + } + grouped_items[group].push(item); + }) $.each(grouped_items, function (group, items) { if (group.length) { @@ -143,7 +155,9 @@ } $.each(items, function (index, item) { - self._renderItemData(ul, item); + if ( $.isFunction(self._renderItemData) ) { + self._renderItemData(ul, item); + } }); }); } @@ -171,7 +185,7 @@ Drupal.behaviors.linkit_autocomplete = { attach: function (context) { // Act on textfields with the "form-linkit-autocomplete" class. - var $autocomplete = $(context).find('input.form-linkit-autocomplete').once('linkit-autocomplete'); + var $autocomplete = $(once('linkit-autocomplete', 'input.form-linkit-autocomplete', context)); if ($autocomplete.length) { $.widget('ui.autocomplete', $.ui.autocomplete, { _create: function () { @@ -182,19 +196,25 @@ _renderItem: autocomplete.options.renderItem }); - // Use jQuery UI Autocomplete on the textfield. - $autocomplete.autocomplete(autocomplete.options); - - $autocomplete.click(function () { - var $this = $(this); - $this.autocomplete('search', $this.val()); - }); - // Process each item. $autocomplete.each(function () { var $uri = $(this); + + // Use jQuery UI Autocomplete on the textfield. + $uri.autocomplete(autocomplete.options); $uri.autocomplete('widget').addClass('linkit-ui-autocomplete'); + $uri.click(function () { + $uri.autocomplete('search', $uri.val()); + }); + + $uri.on('compositionstart.autocomplete', function () { + autocomplete.options.isComposing = true; + }); + $uri.on('compositionend.autocomplete', function () { + autocomplete.options.isComposing = false; + }); + $uri.closest('.form-item').siblings('.form-type-textfield').find('.linkit-widget-title') .each(function() { // Set automatic title flag if title is the same as uri text. @@ -209,20 +229,12 @@ $(this).removeClass('link-widget-title--auto'); }); }); - - $autocomplete.on('compositionstart.autocomplete', function () { - autocomplete.options.isComposing = true; - }); - $autocomplete.on('compositionend.autocomplete', function () { - autocomplete.options.isComposing = false; - }); } }, detach: function (context, settings, trigger) { if (trigger === 'unload') { - $(context).find('input.form-linkit-autocomplete') - .removeOnce('linkit-autocomplete') - .autocomplete('destroy'); + once.remove('linkit-autocomplete', 'input.form-linkit-autocomplete', context) + .forEach((autocomplete) => $(autocomplete).autocomplete('destroy')); } } }; @@ -247,4 +259,4 @@ } }; -})(jQuery, Drupal, _); +})(jQuery, Drupal, once); diff --git a/web/modules/linkit/js/linkit.filter_html.admin.js b/web/modules/linkit/js/linkit.filter_html.admin.js index 3807f6edd17a6ce1411ca166ce6b903b0770c213..8295286fa87320b623a6624739e2b1b518efddf6 100644 --- a/web/modules/linkit/js/linkit.filter_html.admin.js +++ b/web/modules/linkit/js/linkit.filter_html.admin.js @@ -3,7 +3,7 @@ * Send events to add or remove a tags to the filter_html allowed tags. */ -(function ($, Drupal, document) { +(function ($, Drupal, document, once) { 'use strict'; @@ -19,12 +19,15 @@ attach: function (context) { var selector = '[data-drupal-selector="edit-filters-linkit-status"]'; var feature = editorFeature(); - $(context).find(selector).once('filters-linkit-status').each(function () { - $(this).on('click', function () { - var eventName = $(this).is(':checked') ? 'drupalEditorFeatureAdded' : 'drupalEditorFeatureRemoved'; - $(document).trigger(eventName, feature); + + once('filters-linkit-status', selector, context) + .forEach((checkbox) => { + const $checkbox = $(checkbox); + $checkbox.on('click', function () { + var eventName = $(this).is(':checked') ? 'drupalEditorFeatureAdded' : 'drupalEditorFeatureRemoved'; + $(document).trigger(eventName, feature); + }); }); - }); } }; @@ -48,4 +51,4 @@ return linkitFeature; } -})(jQuery, Drupal, document); +})(jQuery, Drupal, document, once); diff --git a/web/modules/linkit/js/scripts/manifest.js b/web/modules/linkit/js/scripts/manifest.js new file mode 100644 index 0000000000000000000000000000000000000000..1065ea5bcea6e2014cfaf62940de90c6a4d2b4f8 --- /dev/null +++ b/web/modules/linkit/js/scripts/manifest.js @@ -0,0 +1,44 @@ +// CKEditor 5 plugins require a manifest file, which must be generated from +// CKEditor source, and typically requires several manual steps. That process is +// automated here. + +const fs = require('fs'); +const { exec } = require('child_process'); + +const manifestPath = + './node_modules/ckeditor5/build/ckeditor5-dll.manifest.json'; + +if (!fs.existsSync(manifestPath)) { + console.log( + 'CKEditor manifest not available. Generating one now. This takes a while, but should only need to happen once.', + ); + exec( + 'yarn --cwd ./node_modules/ckeditor5 install', + (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + + console.log(stdout); + exec( + 'yarn --cwd ./node_modules/ckeditor5 dll:build', + (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + + console.log(stdout); + if (fs.existsSync(manifestPath)) { + console.log(`Manifest created at ${manifestPath}`); + } else { + console.log('error: Unable to create manifest.'); + } + }, + ); + }, + ); +} else { + console.log(`Manifest present at ${manifestPath}`); +} diff --git a/web/modules/linkit/linkit.ckeditor5.yml b/web/modules/linkit/linkit.ckeditor5.yml new file mode 100644 index 0000000000000000000000000000000000000000..5e00227863def2618d5ae2637ad10f1aba2ce946 --- /dev/null +++ b/web/modules/linkit/linkit.ckeditor5.yml @@ -0,0 +1,15 @@ +linkit_extension: + ckeditor5: + plugins: + - linkit.Linkit + drupal: + label: Linkit + library: linkit/ckeditor5 + class: Drupal\linkit\Plugin\CKEditor5Plugin\Linkit + elements: + - <a data-entity-type data-entity-uuid data-entity-substitution> + conditions: + requiresConfiguration: + linkit_enabled: true + plugins: + - ckeditor5_link diff --git a/web/modules/linkit/linkit.info.yml b/web/modules/linkit/linkit.info.yml index b0dd72c307fe9c0154867fbe371319195c5d5045..75caf7533eb8041cac97c466b1444fdd8d66e5f9 100644 --- a/web/modules/linkit/linkit.info.yml +++ b/web/modules/linkit/linkit.info.yml @@ -2,12 +2,13 @@ name: Linkit type: module description: 'Provides an easy interface for internal and external linking with wysiwyg editors.' package: User interface -core_version_requirement: ^8.8 || ^9 +core_version_requirement: ^9.4 || <10.1 configure: entity.linkit_profile.collection test_dependencies: - imce:imce + - drupal:ckeditor -# Information added by Drupal.org packaging script on 2021-09-28 -version: '8.x-5.0-beta13' +# Information added by Drupal.org packaging script on 2023-07-07 +version: '6.0.0' project: 'linkit' -datestamp: 1632848793 +datestamp: 1688748027 diff --git a/web/modules/linkit/linkit.libraries.yml b/web/modules/linkit/linkit.libraries.yml index 9e00dd3101bd8c7cc809f7b118b0175fb3c999c0..bb04aedbdebad1b16dfc1159190afdc1ca6bf728 100644 --- a/web/modules/linkit/linkit.libraries.yml +++ b/web/modules/linkit/linkit.libraries.yml @@ -23,10 +23,26 @@ linkit.autocomplete: dependencies: - linkit/linkit.base - core/drupal.ajax - - core/jquery.ui.autocomplete - - core/underscore + - core/drupal.autocomplete + - core/once linkit.filter_html.admin: version: VERSION js: js/linkit.filter_html.admin.js: {} + dependencies: + - core/jquery + - core/drupal + - core/once + +ckeditor5: + version: VERSION + js: + js/build/linkit.js: { minified: true } + css: + component: + css/linkit.autocomplete.css: {} + dependencies: + - core/jquery + - core/drupal.autocomplete + - core/drupal.ajax diff --git a/web/modules/linkit/linkit.module b/web/modules/linkit/linkit.module index a20f8520645026f64e94c9841e900d019400b1ff..12dab6127717cf39025242133f3f2970aa08f081 100644 --- a/web/modules/linkit/linkit.module +++ b/web/modules/linkit/linkit.module @@ -83,7 +83,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s $form['href_dirty_check'] = [ '#type' => 'hidden', - '#default_value' => isset($input['href']) ? $input['href'] : '', + '#default_value' => $input['href'] ?? '', ]; $form['attributes']['href'] = array_merge($form['attributes']['href'], [ @@ -94,7 +94,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s 'linkit_profile_id' => $linkit_profile_id, ], "#weight" => -10, - '#default_value' => isset($input['href']) ? $input['href'] : '', + '#default_value' => $input['href'] ?? '', ]); $fields = [ @@ -109,7 +109,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s $form['attributes'][$field_name] = [ '#title' => $field_name, '#type' => 'hidden', - '#default_value' => isset($input[$field_name]) ? $input[$field_name] : '', + '#default_value' => $input[$field_name] ?? '', ]; } @@ -133,6 +133,7 @@ function linkit_form_editor_link_dialog_submit(array &$form, FormStateInterface } $fields = [ + 'href', 'data-entity-type', 'data-entity-uuid', 'data-entity-substitution', @@ -149,5 +150,4 @@ function linkit_form_editor_link_dialog_submit(array &$form, FormStateInterface } } } - } diff --git a/web/modules/linkit/package.json b/web/modules/linkit/package.json new file mode 100644 index 0000000000000000000000000000000000000000..cb8575c126f9adac4f7eb43510d16ced43b7d5c2 --- /dev/null +++ b/web/modules/linkit/package.json @@ -0,0 +1,20 @@ +{ + "name": "drupal-linkit", + "version": "5.0.0", + "description": "Provides an easy interface for internal and external linking with wysiwyg editors.", + "author": "", + "license": "GPL-2.0-or-later", + "scripts": { + "watch": "yarn manifest && webpack --mode development --watch", + "build": "yarn manifest && webpack", + "manifest": "node ./js/scripts/manifest.js" + }, + "devDependencies": { + "@ckeditor/ckeditor5-dev-utils": "^30.0.0", + "ckeditor5": "^35.4.0", + "raw-loader": "^4.0.2", + "terser-webpack-plugin": "^5.3.3", + "webpack": "^5.51.1", + "webpack-cli": "^5.0.0" + } +} diff --git a/web/modules/linkit/src/Element/Linkit.php b/web/modules/linkit/src/Element/Linkit.php index 2bb7963f62a7462fb8e27fcea20c6163ab05739e..7824dd80b1da366ade8e12799a7cba6279ce3cbf 100644 --- a/web/modules/linkit/src/Element/Linkit.php +++ b/web/modules/linkit/src/Element/Linkit.php @@ -58,7 +58,7 @@ public static function processLinkitAutocomplete(&$element, FormStateInterface $ $access = FALSE; if (!empty($element['#autocomplete_route_name'])) { - $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : []; + $parameters = $element['#autocomplete_route_parameters'] ?? []; $url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE); /** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */ $access_manager = \Drupal::service('access_manager'); diff --git a/web/modules/linkit/src/Entity/Profile.php b/web/modules/linkit/src/Entity/Profile.php index 070e98472ab8b23a7285e7a04f3b3f2bad65d48f..ee31f47547830a47733ebdabae79e12c2b741b7b 100644 --- a/web/modules/linkit/src/Entity/Profile.php +++ b/web/modules/linkit/src/Entity/Profile.php @@ -35,8 +35,8 @@ * "delete-form" = "/admin/config/content/linkit/manage/{linkit_profile}/delete" * }, * config_export = { - * "id", * "label", + * "id", * "description", * "matchers" * } diff --git a/web/modules/linkit/src/MatcherBase.php b/web/modules/linkit/src/MatcherBase.php index 1176e084ffb6b7fca81dee3274d31a2f09803b27..2f2a74a4cd3061e30b162c85afb9d4b392301d00 100644 --- a/web/modules/linkit/src/MatcherBase.php +++ b/web/modules/linkit/src/MatcherBase.php @@ -87,10 +87,10 @@ public function setWeight($weight) { */ public function getConfiguration() { return [ - 'uuid' => $this->getUuid(), 'id' => $this->getPluginId(), - 'weight' => $this->getWeight(), + 'uuid' => $this->getUuid(), 'settings' => $this->configuration, + 'weight' => $this->getWeight(), ]; } diff --git a/web/modules/linkit/src/Plugin/CKEditor4To5Upgrade/Linkit.php b/web/modules/linkit/src/Plugin/CKEditor4To5Upgrade/Linkit.php new file mode 100644 index 0000000000000000000000000000000000000000..bd6062324ff2798b39ed82f0e65235e4bf25bffe --- /dev/null +++ b/web/modules/linkit/src/Plugin/CKEditor4To5Upgrade/Linkit.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\linkit\Plugin\CKEditor4To5Upgrade; + +use Drupal\ckeditor5\HTMLRestrictions; +use Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface; +use Drupal\Core\Plugin\PluginBase; +use Drupal\filter\FilterFormatInterface; + +/** + * Provides the CKEditor 4 to 5 upgrade for Linkit's CKEditor plugin. + * + * @CKEditor4To5Upgrade( + * id = "linkit", + * cke4_plugin_settings = { + * "drupallink", + * } + * ) + */ +class Linkit extends PluginBase implements CKEditor4To5UpgradePluginInterface { + + /** + * {@inheritdoc} + */ + public function mapCKEditor4ToolbarButtonToCKEditor5ToolbarItem(string $cke4_button, HTMLRestrictions $text_format_html_restrictions): ?array { + throw new \OutOfBoundsException(); + } + + /** + * {@inheritdoc} + */ + public function mapCKEditor4SettingsToCKEditor5Configuration(string $cke4_plugin_id, array $cke4_plugin_settings): ?array { + switch ($cke4_plugin_id) { + // @see \Drupal\linkit\Plugin\CKEditorPlugin\LinkitDrupalLink + // @see \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit + case 'drupallink': + $sanitized = []; + if (!isset($cke4_plugin_settings['linkit_enabled']) || !isset($cke4_plugin_settings['linkit_profile'])) { + $sanitized['linkit_enabled'] = FALSE; + } + else { + $sanitized['linkit_enabled'] = (bool) $cke4_plugin_settings['linkit_enabled']; + if ($sanitized['linkit_enabled']) { + $sanitized['linkit_profile'] = $cke4_plugin_settings['linkit_profile']; + } + } + return ['linkit_extension' => $sanitized]; + + default: + throw new \OutOfBoundsException(); + } + } + + /** + * {@inheritdoc} + */ + public function computeCKEditor5PluginSubsetConfiguration(string $cke5_plugin_id, FilterFormatInterface $text_format): ?array { + throw new \OutOfBoundsException(); + } + +} diff --git a/web/modules/linkit/src/Plugin/CKEditor5Plugin/Linkit.php b/web/modules/linkit/src/Plugin/CKEditor5Plugin/Linkit.php new file mode 100644 index 0000000000000000000000000000000000000000..a0cc779793888c082b0e2cdf84edf2f6ef1c9151 --- /dev/null +++ b/web/modules/linkit/src/Plugin/CKEditor5Plugin/Linkit.php @@ -0,0 +1,157 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\linkit\Plugin\CKEditor5Plugin; + +use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait; +use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault; +use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Url; +use Drupal\editor\EditorInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * CKEditor 5 Linkit plugin configuration. + */ +class Linkit extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, ContainerFactoryPluginInterface { + + use CKEditor5PluginConfigurableTrait; + + /** + * The Linkit profile storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $linkitProfileStorage; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $linkit_profile_storage) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->linkitProfileStorage = $linkit_profile_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager')->getStorage('linkit_profile') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $all_profiles = $this->linkitProfileStorage->loadMultiple(); + + $options = []; + foreach ($all_profiles as $profile) { + $options[$profile->id()] = $profile->label(); + } + + $form['linkit_profile'] = [ + '#wrapper_attributes' => ['class' => ['container-inline']], + '#type' => 'select', + '#title' => $this->t('Linkit profile'), + '#options' => $options, + '#default_value' => $this->configuration['linkit_profile'] ?? '', + '#empty_option' => $this->t('- Linkit disabled -'), + ]; + + return $form; + } + + /** + * Config validation callback: require linkit_profile if linkit_enabled=TRUE. + * + * @param array $values + * The configuration subtree for ckeditor5.plugin.linkit_extension. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + * + * @see linkit.schema.yml + */ + public static function requireProfileIfEnabled(array $values, ExecutionContextInterface $context): void { + if ($values['linkit_enabled'] === TRUE && empty($values['linkit_profile'])) { + $context->buildViolation(t('Linkit is enabled, please select the Linkit profile you wish to use.')) + ->atPath('linkit_profile') + ->addViolation(); + } + elseif ($values['linkit_enabled'] === FALSE && !empty($values['linkit_profile'])) { + $context->buildViolation(t('Linkit is disabled; it does not make sense to associate a Linkit profile.')) + ->atPath('linkit_profile') + ->addViolation(); + } + } + + /** + * Computes all valid choices for the "linkit_profile" setting. + * + * @see linkit.schema.yml + * + * @return string[] + * All valid choices. + */ + public static function validChoices(): array { + $linkit_profile_storage = \Drupal::service('entity_type.manager')->getStorage('linkit_profile'); + assert($linkit_profile_storage instanceof EntityStorageInterface); + return array_keys($linkit_profile_storage->loadMultiple()); + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Match the config schema structure at ckeditor5.plugin.linkit_extension. + if (empty($form_state->getValue('linkit_profile'))) { + $form_state->unsetValue('linkit_profile'); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['linkit_enabled'] = $form_state->hasValue('linkit_profile'); + // `linkit_profile` only is relevant when Linkit is enabled. + if ($this->configuration['linkit_enabled']) { + $this->configuration['linkit_profile'] = $form_state->getValue('linkit_profile'); + } + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'linkit_enabled' => FALSE, + ]; + } + + /** + * {@inheritdoc} + */ + public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array { + assert($this->configuration['linkit_enabled'] === TRUE); + return [ + 'linkit' => [ + 'profile' => $this->configuration['linkit_profile'], + 'autocompleteUrl' => Url::fromRoute('linkit.autocomplete', ['linkit_profile_id' => $this->configuration['linkit_profile']]) + ->toString(TRUE) + ->getGeneratedUrl() + ], + ]; + } + +} diff --git a/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php b/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php index c0168bcf8ae7af2469c6ad51d4f6fb01225ece28..1b4c24f0b7a53bb4065c3d1392e7c33d27ad6e1a 100644 --- a/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php +++ b/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php @@ -58,7 +58,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor $form['linkit_enabled'] = [ '#type' => 'checkbox', '#title' => $this->t('Linkit enabled'), - '#default_value' => isset($settings['plugins']['drupallink']['linkit_enabled']) ? $settings['plugins']['drupallink']['linkit_enabled'] : '', + '#default_value' => $settings['plugins']['drupallink']['linkit_enabled'] ?? '', '#description' => $this->t('Enable Linkit for this text format.'), ]; @@ -66,7 +66,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor '#type' => 'select', '#title' => $this->t('Linkit profile'), '#options' => $options, - '#default_value' => isset($settings['plugins']['drupallink']['linkit_profile']) ? $settings['plugins']['drupallink']['linkit_profile'] : '', + '#default_value' => $settings['plugins']['drupallink']['linkit_profile'] ?? '', '#empty_option' => $this->t('- Select -'), '#description' => $this->t('Select the Linkit profile you wish to use with this text format.'), '#states' => [ diff --git a/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php b/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php index c1070349b7f980a706c4a7a09346f417cedc9041..87e40f752672548fed93c1ae5a515c988c74390e 100644 --- a/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php +++ b/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php @@ -2,12 +2,16 @@ namespace Drupal\linkit\Plugin\Field\FieldFormatter; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Path\PathValidatorInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\link\LinkItemInterface; use Drupal\link\Plugin\Field\FieldFormatter\LinkFormatter; -use Drupal\linkit\Entity\Profile; +use Drupal\linkit\ProfileInterface; use Drupal\linkit\SubstitutionManagerInterface; use Drupal\linkit\Utility\LinkitHelper; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -33,20 +37,59 @@ class LinkitFormatter extends LinkFormatter { protected $substitutionManager; /** - * The entity type manager. + * The linkit profile storage service. * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * @var \Drupal\Core\Entity\EntityStorageInterface */ - protected $entityTypeManager; + protected $linkitProfileStorage; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); - $instance->substitutionManager = $container->get('plugin.manager.linkit.substitution'); - $instance->entityTypeManager = $container->get('entity_type.manager'); - return $instance; + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('path.validator'), + $container->get('entity_type.manager'), + $container->get('plugin.manager.linkit.substitution') + ); + } + + /** + * Constructs a new Linkit field formatter. + * + * @param string $plugin_id + * The plugin_id for the formatter. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the formatter is associated. + * @param array $settings + * The formatter settings. + * @param string $label + * The formatter label display setting. + * @param string $view_mode + * The view mode. + * @param array $third_party_settings + * Third party settings. + * @param \Drupal\Core\Path\PathValidatorInterface $path_validator + * The path validator service. + * @param Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager service. + * @param \Drupal\linkit\SubstitutionManagerInterface $substitution_manager + * The substitution manager. + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, PathValidatorInterface $path_validator, EntityTypeManagerInterface $entityTypeManager, SubstitutionManagerInterface $substitution_manager) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $path_validator); + + $this->substitutionManager = $substitution_manager; + $this->linkitProfileStorage = $entityTypeManager->getStorage('linkit_profile'); } /** @@ -64,12 +107,9 @@ public static function defaultSettings() { public function settingsForm(array $form, FormStateInterface $form_state) { $elements = parent::settingsForm($form, $form_state); - $linkit_profiles = $this->entityTypeManager->getStorage('linkit_profile')->loadMultiple(); - - $options = []; - foreach ($linkit_profiles as $linkit_profile) { - $options[$linkit_profile->id()] = $linkit_profile->label(); - } + $options = array_map(function ($linkit_profile) { + return $linkit_profile->label(); + }, $this->linkitProfileStorage->loadMultiple()); $elements['linkit_profile'] = [ '#type' => 'select', @@ -89,7 +129,7 @@ public function settingsSummary() { $summary = parent::settingsSummary(); $linkit_profile_id = $this->getSetting('linkit_profile'); - $linkit_profile = $this->entityTypeManager->getStorage('linkit_profile')->load($linkit_profile_id); + $linkit_profile = $this->linkitProfileStorage->load($linkit_profile_id); if ($linkit_profile) { $summary[] = $this->t('Linkit profile: @linkit_profile', ['@linkit_profile' => $linkit_profile->label()]); @@ -110,7 +150,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $link_item = $items->get($delta); $substituted_url = $this->getSubstitutedUrl($link_item); // Convert generated URL into a URL object. - if ($substituted_url && ($url = \Drupal::pathValidator()->getUrlIfValid($substituted_url->getGeneratedUrl()))) { + if ($substituted_url && ($url = $this->pathValidator->getUrlIfValid($substituted_url->getGeneratedUrl()))) { // Keep query and fragment. $parsed_url = parse_url($link_item->uri); if (!empty($parsed_url['query'])) { @@ -151,15 +191,26 @@ public function viewElements(FieldItemListInterface $items, $langcode) { * The substitution URL, or NULL if not able to retrieve it from the item. */ protected function getSubstitutedUrl(LinkItemInterface $item) { - if ($entity = LinkitHelper::getEntityFromUserInput($item->uri)) { - $profile = Profile::load($this->getSettings()['linkit_profile']); + // First try to derive entity information from Linkit-specific attributes. + // This is more reliable and is required for File entities. + if (!empty($item->options['data-entity-type']) && !empty($item->options['data-entity-uuid'])) { + $entity = \Drupal::service('entity.repository')->loadEntityByUuid($item->options['data-entity-type'], $item->options['data-entity-uuid']); + } + else { + $entity = LinkitHelper::getEntityFromUserInput($item->uri); + } + if ($entity instanceof EntityInterface) { + $linkit_profile = $this->linkitProfileStorage->load($this->getSettings()['linkit_profile']); - /** @var \\Drupal\linkit\Plugin\Linkit\Matcher\EntityMatcher $matcher */ - $matcher = $profile->getMatcherByEntityType($entity->getEntityTypeId()); + if (!$linkit_profile instanceof ProfileInterface) { + return NULL; + } + + /** @var \Drupal\linkit\Plugin\Linkit\Matcher\EntityMatcher $matcher */ + $matcher = $linkit_profile->getMatcherByEntityType($entity->getEntityTypeId()); $substitution_type = $matcher ? $matcher->getConfiguration()['settings']['substitution_type'] : SubstitutionManagerInterface::DEFAULT_SUBSTITUTION; return $this->substitutionManager->createInstance($substitution_type)->getUrl($entity); } - return NULL; } diff --git a/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php b/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php index f67867e7e524c3701076ddbbec03741699e6c4e7..c9e46203144be5b31fd6657a57ee446bdd33cd1f 100644 --- a/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php +++ b/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php @@ -2,11 +2,16 @@ namespace Drupal\linkit\Plugin\Field\FieldWidget; -use Drupal\Core\Url; -use Drupal\link\Plugin\Field\FieldWidget\LinkWidget; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\file\FileInterface; +use Drupal\link\Plugin\Field\FieldWidget\LinkWidget; use Drupal\linkit\Utility\LinkitHelper; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'linkit' widget. @@ -22,118 +27,69 @@ class LinkitWidget extends LinkWidget { /** - * {@inheritdoc} + * The current user. + * + * @var \Drupal\Core\Session\AccountProxyInterface */ - public static function defaultSettings() { - return [ - 'linkit_profile' => 'default', - 'linkit_auto_link_text' => FALSE, - ] + parent::defaultSettings(); - } + protected $currentUser; /** - * {@inheritdoc} + * The linkit profile storage service. + * + * @var \Drupal\Core\Entity\EntityStorageInterface */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $item = $items[$delta]; - $uri = $item->uri; - $uri_scheme = parse_url($uri, PHP_URL_SCHEME); - $is_nolink = substr($uri, 0, 14) === 'route:<nolink>'; - if (!empty($uri) && empty($uri_scheme) && $is_nolink) { - $uri = LinkitHelper::uriFromUserInput($uri); - $uri_scheme = parse_url($uri, PHP_URL_SCHEME); - } - if ($is_nolink) { - $uri_as_url = $uri; - } - else { - $uri_as_url = !empty($uri) ? static::getUriAsDisplayableString($uri) : ''; - } - $linkit_profile_id = $this->getSetting('linkit_profile'); - - // The current field value could have been entered by a different user. - // However, if it is inaccessible to the current user, do not display it - // to them. - $default_allowed = !$item->isEmpty() && (\Drupal::currentUser()->hasPermission('link to any page') || $item->getUrl()->access()); - - if ($default_allowed && $uri_scheme == 'entity') { - $entity = LinkitHelper::getEntityFromUri($uri); - } - - $element['uri'] = [ - '#type' => 'linkit', - '#title' => $this->t('URL'), - '#placeholder' => $this->getSetting('placeholder_url'), - '#default_value' => $default_allowed ? $uri_as_url : NULL, - '#maxlength' => 2048, - '#required' => $element['#required'], - '#description' => $this->t('Start typing to find content or paste a URL and click on the suggestion below.'), - '#autocomplete_route_name' => 'linkit.autocomplete', - '#autocomplete_route_parameters' => [ - 'linkit_profile_id' => $linkit_profile_id, - ], - '#error_no_message' => TRUE, - ]; - - $element['attributes']['href'] = [ - '#type' => 'hidden', - '#default_value' => $default_allowed ? $uri : '', - ]; - - $element['attributes']['data-entity-type'] = [ - '#type' => 'hidden', - '#default_value' => $default_allowed && isset($entity) ? $entity->getEntityTypeId() : '', - ]; - - $element['attributes']['data-entity-uuid'] = [ - '#type' => 'hidden', - '#default_value' => $default_allowed && isset($entity) ? $entity->uuid() : '', - ]; - - $element['attributes']['data-entity-substitution'] = [ - '#type' => 'hidden', - '#default_value' => $default_allowed && isset($entity) ? $entity->getEntityTypeId() == 'file' ? 'file' : 'canonical' : '', - ]; + protected $linkitProfileStorage; - $element['title'] = [ - '#type' => 'textfield', - '#title' => $this->t('Link text'), - '#placeholder' => $this->getSetting('placeholder_title'), - '#default_value' => isset($items[$delta]->title) ? $items[$delta]->title : NULL, - '#maxlength' => 255, - '#access' => $this->getFieldSetting('title') != DRUPAL_DISABLED, - '#required' => $this->getFieldSetting('title') === DRUPAL_REQUIRED && $element['#required'], - '#attributes' => [ - 'class' => ['linkit-widget-title'], - ], - '#error_no_message' => TRUE, - ]; - if ($this->getSetting('linkit_auto_link_text')) { - $element['title']['#attributes']['class'][] = 'linkit-widget-title--autofill-enabled'; - } - // Post-process the title field to make it conditionally required if URL is - // non-empty. Omit the validation on the field edit form, since the field - // settings cannot be saved otherwise. - if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') == DRUPAL_REQUIRED) { - $element['#element_validate'][] = [get_called_class(), 'validateTitleElement']; - } + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('current_user'), + $container->get('entity_type.manager') + ); + } - // If cardinality is 1, ensure a proper label is output for the field. - if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) { - // If the link title is disabled, use the field definition label as the - // title of the 'uri' element. - if ($this->getFieldSetting('title') == DRUPAL_DISABLED) { - $element['uri']['#title'] = $element['#title']; - } - // Otherwise wrap everything in a details element. - else { - $element += [ - '#type' => 'fieldset', - ]; - } - } + /** + * Constructs a new Linkit field widget. + * + * @param string $plugin_id + * The plugin_id for the formatter. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the formatter is associated. + * @param array $settings + * The widget settings. + * @param array $third_party_settings + * The widget third party settings. + * @param \Drupal\Core\Session\AccountProxyInterface $currentUser + * The current user. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager service. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountProxyInterface $currentUser, EntityTypeManagerInterface $entityTypeManager) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + $this->currentUser = $currentUser; + $this->linkitProfileStorage = $entityTypeManager->getStorage('linkit_profile'); + } - return $element; + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'linkit_profile' => 'default', + 'linkit_auto_link_text' => FALSE, + ] + parent::defaultSettings(); } /** @@ -142,12 +98,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen public function settingsForm(array $form, FormStateInterface $form_state) { $elements = parent::settingsForm($form, $form_state); - $linkit_profiles = \Drupal::entityTypeManager()->getStorage('linkit_profile')->loadMultiple(); - - $options = []; - foreach ($linkit_profiles as $linkit_profile) { - $options[$linkit_profile->id()] = $linkit_profile->label(); - } + $options = array_map(function ($linkit_profile) { + return $linkit_profile->label(); + }, $this->linkitProfileStorage->loadMultiple()); $elements['linkit_profile'] = [ '#type' => 'select', @@ -171,7 +124,7 @@ public function settingsSummary() { $summary = parent::settingsSummary(); $linkit_profile_id = $this->getSetting('linkit_profile'); - $linkit_profile = \Drupal::entityTypeManager()->getStorage('linkit_profile')->load($linkit_profile_id); + $linkit_profile = $this->linkitProfileStorage->load($linkit_profile_id); if ($linkit_profile) { $summary[] = $this->t('Linkit profile: @linkit_profile', ['@linkit_profile' => $linkit_profile->label()]); @@ -189,28 +142,84 @@ public function settingsSummary() { /** * {@inheritdoc} */ - public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { - foreach ($values as &$value) { - $value['uri'] = LinkitHelper::uriFromUserInput($value['uri']); - $value += ['options' => []]; + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + + /** @var \Drupal\link\LinkItemInterface $item */ + $item = $items[$delta]; + $uri = $item->uri ?? NULL; + + // Try to fetch entity information from the URI. + $default_allowed = !$item->isEmpty() && ($this->currentUser->hasPermission('link to any page') || $item->getUrl()->access()); + if (!empty($item->options['data-entity-type']) && !empty($item->options['data-entity-uuid'])) { + $entity = \Drupal::service('entity.repository')->loadEntityByUuid($item->options['data-entity-type'], $item->options['data-entity-uuid']); } - return $values; + else { + $entity = $default_allowed && $uri ? LinkitHelper::getEntityFromUri($uri) : NULL; + } + // Display entity URL consistently across all entity types. + if ($entity instanceof FileInterface) { + // File entities are anomalies, so we handle them differently. + $element['uri']['#default_value'] = \Drupal::service('file_url_generator')->generateString($entity->getFileUri()); + } + elseif ($entity instanceof EntityInterface) { + $uri_parts = parse_url($uri); + $uri_options = []; + // Extract query parameters and fragment and merge them into $uri_options. + if (isset($uri_parts['fragment']) && $uri_parts['fragment'] !== '') { + $uri_options += ['fragment' => $uri_parts['fragment']]; + } + if (!empty($uri_parts['query'])) { + $uri_query = []; + parse_str($uri_parts['query'], $uri_query); + $uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query; + } + $element['uri']['#default_value'] = $entity->toUrl()->setOptions($uri_options)->toString(); + } + // Change the URI field to use the linkit profile. + $element['uri']['#type'] = 'linkit'; + $element['uri']['#description'] = $this->t('Start typing to find content or paste a URL and click on the suggestion below.'); + $element['uri']['#autocomplete_route_name'] = 'linkit.autocomplete'; + $element['uri']['#autocomplete_route_parameters'] = [ + 'linkit_profile_id' => $this->getSetting('linkit_profile'), + ]; + + // Add a class to the title field. + $element['title']['#attributes']['class'][] = 'linkit-widget-title'; + if ($this->getSetting('linkit_auto_link_text')) { + $element['title']['#attributes']['data-linkit-widget-title-autofill-enabled'] = TRUE; + } + + // Add linkit specific attributes. + $element['attributes']['href'] = [ + '#type' => 'hidden', + '#default_value' => $default_allowed ? $uri : '', + ]; + $element['attributes']['data-entity-type'] = [ + '#type' => 'hidden', + '#default_value' => $entity ? $entity->getEntityTypeId() : '', + ]; + $element['attributes']['data-entity-uuid'] = [ + '#type' => 'hidden', + '#default_value' => $entity ? $entity->uuid() : '', + ]; + $element['attributes']['data-entity-substitution'] = [ + '#type' => 'hidden', + '#default_value' => $entity ? ($entity->getEntityTypeId() === 'file' ? 'file' : 'canonical') : '', + ]; + + return $element; } /** * {@inheritdoc} */ - protected static function getUriAsDisplayableString($uri) { - $scheme = parse_url($uri, PHP_URL_SCHEME); - if ($scheme === 'base') { - $uri_reference = explode(':', $uri, 2)[1]; - $uri = 'internal:' . $uri_reference; - } - elseif ($scheme === 'entity') { - $uri_reference = explode(':', $uri, 2)[1]; - $uri = '/' . $uri_reference; + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + foreach ($values as &$value) { + $value['uri'] = LinkitHelper::uriFromUserInput($value['uri']); + $value += ['options' => $value['attributes']]; } - return parent::getUriAsDisplayableString($uri); + return $values; } } diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php index d6ea3613069e0f6446c58f7598968097ffcb3f7b..c9513d94d04f7cd0bb58273b5c252a7c2525145b 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php +++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php @@ -23,14 +23,11 @@ class EmailMatcher extends MatcherBase { public function execute($string) { $suggestions = new SuggestionCollection(); - // Strip the mailto: prefix to match only the e-mail part of the string. - $string = str_replace('mailto:', '', $string); - // Check for an e-mail address then return an e-mail match and create a // mail-to link if appropriate. if (filter_var($string, FILTER_VALIDATE_EMAIL)) { $suggestion = new DescriptionSuggestion(); - $suggestion->setLabel($string) + $suggestion->setLabel($this->t('E-mail @email', ['@email' => $string])) ->setPath('mailto:' . Html::escape($string)) ->setGroup($this->t('E-mail')) ->setDescription($this->t('Opens your mail client ready to e-mail @email', ['@email' => $string])); diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php index db46595308b70ed8432ef24f21e91f3f0d654c1b..e48c6afb9e3618e56400f3e0f656683af56cd8ce 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php +++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php @@ -322,6 +322,7 @@ public static function elementValidateFilter(&$element, FormStateInterface $form public function execute($string) { $suggestions = new SuggestionCollection(); $query = $this->buildEntityQuery($string); + $query->accessCheck(TRUE); $query_result = $query->execute(); $url_results = $this->findEntityIdByUrl($string); $result = array_merge($query_result, $url_results); @@ -344,12 +345,6 @@ public function execute($string) { $entity = $this->entityRepository->getTranslationFromContext($entity); $suggestion = $this->createSuggestion($entity); - if ($query = parse_url($string, PHP_URL_QUERY)) { - $suggestion->setPath($suggestion->getPath() . '?' . $query); - } - if ($fragment = parse_url($string, PHP_URL_FRAGMENT)) { - $suggestion->setPath($suggestion->getPath() . '#' . $fragment); - } $suggestions->addSuggestion($suggestion); } @@ -371,6 +366,7 @@ protected function buildEntityQuery($search_string) { $entity_type = $this->entityTypeManager->getDefinition($this->targetType); $query = $this->entityTypeManager->getStorage($this->targetType)->getQuery(); + $query->accessCheck(TRUE); $label_key = $entity_type->getKey('label'); if ($label_key) { diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php index 9d05089982a2c81c44be38f99e6b9777a33ae35d..cc0fba30f427aa7060e74ad9cc60967a256c5049 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php +++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\file\FileInterface; use Drupal\image\Entity\ImageStyle; use Drupal\linkit\Utility\LinkitXss; @@ -57,7 +58,7 @@ public function getSummary() { public function defaultConfiguration() { return [ 'file_extensions' => '', - 'file_status' => FILE_STATUS_PERMANENT, + 'file_status' => FileInterface::STATUS_PERMANENT, 'images' => [ 'show_dimensions' => FALSE, 'show_thumbnail' => FALSE, @@ -226,7 +227,7 @@ protected function buildDescription(EntityInterface $entity) { */ protected function buildPath(EntityInterface $entity) { /** @var \Drupal\file\FileInterface $entity */ - return file_url_transform_relative(file_create_url($entity->getFileUri())); + return \Drupal::service('file_url_generator')->generateString($entity->getFileUri()); } } diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php index 90347b6ed61aaa68c8710e0422338ed9d8d1a0a6..dd3ca15eaac87e86e15ac64a880bdfe848517988 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php +++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php @@ -2,10 +2,10 @@ namespace Drupal\linkit\Plugin\Linkit\Matcher; +use Drupal\Core\Url; use Drupal\linkit\MatcherBase; use Drupal\linkit\Suggestion\DescriptionSuggestion; use Drupal\linkit\Suggestion\SuggestionCollection; -use Drupal\linkit\Utility\LinkitHelper; /** * Provides specific linkit matchers for the front page. @@ -22,18 +22,12 @@ class FrontPageMatcher extends MatcherBase { */ public function execute($string) { $suggestions = new SuggestionCollection(); - $front_path = '/'; - $query_and_fragment = LinkitHelper::getQueryAndFragment($string); - - if (!empty($query_and_fragment)) { - $string = substr($string, 0, strpos($string, $query_and_fragment)); - } // Special for link to front page. - if (strpos($string, 'front') !== FALSE || $string == $front_path) { + if (strpos($string, 'front') !== FALSE) { $suggestion = new DescriptionSuggestion(); $suggestion->setLabel($this->t('Front page')) - ->setPath($front_path . $query_and_fragment) + ->setPath(Url::fromRoute('<front>')->toString()) ->setGroup($this->t('System')) ->setDescription($this->t('The front page for this site.')); diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php index f40c7c006e049a7fbaeb92b579ccdbbbd004f9e1..b09854622766ded57c24772453e86702f693acff 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php +++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php @@ -88,7 +88,7 @@ protected function buildEntityQuery($search_string) { if ($this->configuration['include_unpublished'] == FALSE) { $query->condition('status', NodeInterface::PUBLISHED); } - elseif (count($this->moduleHandler->getImplementations('node_grants')) === 0) { + elseif (!$this->moduleHandler->hasImplementations('node_grants')) { if (($this->currentUser->hasPermission('bypass node access') || $this->currentUser->hasPermission('view any unpublished content'))) { // User can see all content, no check necessary. } diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/NolinkMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/NolinkMatcher.php deleted file mode 100644 index 90eee020b40ee45e700b38f54527c2d9f47f9acb..0000000000000000000000000000000000000000 --- a/web/modules/linkit/src/Plugin/Linkit/Matcher/NolinkMatcher.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php - -namespace Drupal\linkit\Plugin\Linkit\Matcher; - -use Drupal\linkit\MatcherBase; -use Drupal\linkit\Suggestion\DescriptionSuggestion; -use Drupal\linkit\Suggestion\SuggestionCollection; - -/** - * Provides a linkit matcher for route:<nolink>. - * - * @Matcher( - * id = "nolink", - * label = @Translation("Nolink"), - * ) - */ -class NolinkMatcher extends MatcherBase { - - /** - * {@inheritdoc} - */ - public function execute($string) { - $suggestions = new SuggestionCollection(); - - // Check for the text 'nolink' (e.g. like route:<nolink> with core link - // fields) and return route:<nolink> if it exists. - if (strpos($string, 'nolink') !== FALSE) { - $suggestion = new DescriptionSuggestion(); - $suggestion->setLabel($this->t('Empty link')) - ->setPath('route:<nolink>') - ->setGroup($this->t('System')) - ->setDescription($this->t('An empty link')); - - $suggestions->addSuggestion($suggestion); - } - return $suggestions; - } - -} diff --git a/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php b/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php index c441dfc1af22a5a75bfeaa99dd108655e483d217..cfffe04e7920126b34f8b5c85bf8b1e30e6fc767 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php +++ b/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php @@ -24,7 +24,7 @@ class File extends PluginBase implements SubstitutionInterface { public function getUrl(EntityInterface $entity) { $url = new GeneratedUrl(); /** @var \Drupal\file\FileInterface $entity */ - $url->setGeneratedUrl(file_create_url($entity->getFileUri())); + $url->setGeneratedUrl($entity->createFileUrl()); $url->addCacheableDependency($entity); return $url; } diff --git a/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php b/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php index 9cdcb1cead2718470835d9950fec152046f5b880..bdcf1ab36925f7231dcb036eb452914716d2cb6a 100644 --- a/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php +++ b/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php @@ -48,7 +48,7 @@ public function getUrl(EntityInterface $entity) { if ($source_field && $entity->hasField($source_field->getName()) && $entity->get($source_field->getName())->entity instanceof FileInterface) { /** @var \Drupal\file\FileInterface $file */ $file = $entity->get($source_field->getName())->entity; - $url->setGeneratedUrl(file_create_url($file->getFileUri())); + $url->setGeneratedUrl($file->createFileUrl()); $url->addCacheableDependency($entity); return $url; } diff --git a/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php b/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php index 30d6ad0cfc9eaaa4dc9f88c432298af70353a2cd..2fe03d8b5077a32b00e474f13a082d232e88a7a7 100644 --- a/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php +++ b/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php @@ -34,6 +34,7 @@ public function setDescription($description) { /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return parent::jsonSerialize() + [ diff --git a/web/modules/linkit/src/Suggestion/EntitySuggestion.php b/web/modules/linkit/src/Suggestion/EntitySuggestion.php index 772db110f116cb616999f826ad96b076ffe962f8..bd55db3c4e7e45834712eee42d85ab4c9a9284da 100644 --- a/web/modules/linkit/src/Suggestion/EntitySuggestion.php +++ b/web/modules/linkit/src/Suggestion/EntitySuggestion.php @@ -70,6 +70,7 @@ public function setSubstitutionId($substitution_id) { /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return parent::jsonSerialize() + [ 'entity_uuid' => $this->entityUuid, diff --git a/web/modules/linkit/src/Suggestion/SimpleSuggestion.php b/web/modules/linkit/src/Suggestion/SimpleSuggestion.php index 49063906d75c365b598d941f86e2560bc1e84493..db278801e21dc01c9f820ecae26e4e37b5e7c6bf 100644 --- a/web/modules/linkit/src/Suggestion/SimpleSuggestion.php +++ b/web/modules/linkit/src/Suggestion/SimpleSuggestion.php @@ -76,6 +76,7 @@ public function setGroup($group) { /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return [ 'label' => $this->getLabel(), diff --git a/web/modules/linkit/src/Suggestion/SuggestionCollection.php b/web/modules/linkit/src/Suggestion/SuggestionCollection.php index de20548a37d14870bbb62391f288042fb4a5f86b..9117009e6e7239bffa688362512800c9253532e7 100644 --- a/web/modules/linkit/src/Suggestion/SuggestionCollection.php +++ b/web/modules/linkit/src/Suggestion/SuggestionCollection.php @@ -47,6 +47,7 @@ public function addSuggestions(SuggestionCollection $suggestionCollection) { /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return [ 'suggestions' => $this->suggestions, diff --git a/web/modules/linkit/src/SuggestionManager.php b/web/modules/linkit/src/SuggestionManager.php index 82004c594ecfa5ba509cc2cb8a5cb7764e34b9c4..1f7b28084bb8ceb05861c16b5ec1025a724dfc0e 100644 --- a/web/modules/linkit/src/SuggestionManager.php +++ b/web/modules/linkit/src/SuggestionManager.php @@ -54,7 +54,7 @@ public function addUnscathedSuggestion(SuggestionCollection $suggestionCollectio $suggestion = new DescriptionSuggestion(); $suggestion->setLabel(Html::escape($search_string)) ->setGroup($this->t('No results')) - ->setDescription($this->t('Linkit could not find any suggestions. This URL will be used as is.')) + ->setDescription($this->t('No content suggestions found. This URL will be used as is.')) ->setPath($search_string); $suggestionCollection->addSuggestion($suggestion); return $suggestionCollection; diff --git a/web/modules/linkit/src/Utility/LinkitHelper.php b/web/modules/linkit/src/Utility/LinkitHelper.php index 8376ca4048a3e2f1190eccfeae8a517b981537aa..e1ba0f7f4d6c34e05b8ba659cbec1e2ca1de756b 100644 --- a/web/modules/linkit/src/Utility/LinkitHelper.php +++ b/web/modules/linkit/src/Utility/LinkitHelper.php @@ -23,11 +23,12 @@ class LinkitHelper { * uri, or NULL if could not match any entity. */ public static function getEntityFromUri($uri) { - // Stripe out potential query and fragment from the uri. + // Strip out potential query and fragment from the uri. $uri = strtok(strtok($uri, "?"), "#"); // Remove the schema, if any. Otherwise, remove the forwarding "/". if (strpos($uri, 'entity:') !== FALSE) { - list(, $uri) = explode(':', $uri); + $uri_parts = explode(':', $uri); + $uri = $uri_parts[1] ?? $uri; } else { $uri = trim($uri, '/'); @@ -35,9 +36,10 @@ public static function getEntityFromUri($uri) { if ($uri) { $parts = explode('/', $uri, 2); - if (count($parts) == 2 && ($entity_type = $parts[0]) && ($entity_id = $parts[1])) { + if (count($parts) === 2) { + [$entity_type, $entity_id] = $parts; $entity_manager = \Drupal::entityTypeManager(); - if ($entity_manager->getDefinition($entity_type, FALSE)) { + if ($entity_manager->hasDefinition($entity_type)) { if ($entity = $entity_manager->getStorage($entity_type)->load($entity_id)) { return \Drupal::service('entity.repository')->getTranslationFromContext($entity); } @@ -110,17 +112,16 @@ public static function uriFromUserInput($input) { ->getViaScheme('public') ->getDirectoryPath(); - $protocol_matches = []; - preg_match('/^([a-z]*?):/', $input, $protocol_matches); if (!empty($public_files_dir) && strpos($input, "/$public_files_dir") === 0) { return "base:$input"; } - elseif ((count($protocol_matches) > 1 && in_array($protocol_matches[1], UrlHelper::getAllowedProtocols())) || $is_nolink) { + $scheme = parse_url($input, PHP_URL_SCHEME); + // Check if the input already contains a scheme. + if (!empty($scheme)) { return $input; } - else { - return "internal:$input"; - } + + return "internal:$input"; } /** @@ -149,17 +150,18 @@ public static function getEntityFromUserInput($input) { try { $route_name = Url::fromUri($input)->getRouteName(); $params = array_filter(Url::fromUri($input)->getRouteParameters()); - $possibly_an_entity_type = key($params); - // Return only the entity, if this is a canonical route. - if ($route_name === 'entity.' . $possibly_an_entity_type . '.canonical') { - $entity = \Drupal::entityTypeManager() - ->getStorage($possibly_an_entity_type) - ->load($params[$possibly_an_entity_type]); - if (!($entity instanceof EntityInterface)) { - return NULL; + foreach ($params as $possibly_an_entity_type => $possibly_an_entity_id) { + // Return only the entity, if this is a canonical route. + if ($route_name === 'entity.' . $possibly_an_entity_type . '.canonical') { + $entity = \Drupal::entityTypeManager() + ->getStorage($possibly_an_entity_type) + ->load($possibly_an_entity_id); + if (!($entity instanceof EntityInterface)) { + return NULL; + } + return \Drupal::service('entity.repository') + ->getTranslationFromContext($entity); } - return \Drupal::service('entity.repository') - ->getTranslationFromContext($entity); } } catch (\Exception $e) { @@ -185,8 +187,8 @@ public static function getPathByAlias($input) { /** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */ $language_manager = \Drupal::service('language_manager'); + $input_path = parse_url($input, PHP_URL_PATH); foreach ($language_manager->getLanguages() as $language) { - $input_path = parse_url($input, PHP_URL_PATH); if ($prefix = $config->get('url.prefixes.' . $language->getId())) { // Strip the language prefix. $input_path = preg_replace("/^\/$prefix\//", '/', $input_path); diff --git a/web/modules/linkit/tests/linkit_test/linkit_test.info.yml b/web/modules/linkit/tests/linkit_test/linkit_test.info.yml index 5a732b728f10478348207d44c2bee01823d71746..93c3d27ce62cbbb7b5f70ae8da450f08c36610cd 100644 --- a/web/modules/linkit/tests/linkit_test/linkit_test.info.yml +++ b/web/modules/linkit/tests/linkit_test/linkit_test.info.yml @@ -2,13 +2,12 @@ name: 'Linkit test module' description: 'Support module for Linkit testing.' package: Testing type: module -core_version_requirement: ^8.8 || ^9 dependencies: - linkit:linkit - drupal:field - drupal:text -# Information added by Drupal.org packaging script on 2021-09-28 -version: '8.x-5.0-beta13' +# Information added by Drupal.org packaging script on 2023-07-07 +version: '6.0.0' project: 'linkit' -datestamp: 1632848793 +datestamp: 1688748027 diff --git a/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php b/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php index e931e85ee8bfa93f31ee8cd8bc34cada8e86227f..0635e64e78f6088a7076d354431a35b4735362c6 100644 --- a/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php +++ b/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php @@ -24,7 +24,7 @@ class LinkitControllerTest extends LinkitBrowserTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->linkitProfile = $this->createProfile(); diff --git a/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php b/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php index bc61b6c87080d60ff61f2b735f6e2666dcf1bff4..7a669fe17d343bae338c55da80467026ebd67d1e 100644 --- a/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php +++ b/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php @@ -14,7 +14,7 @@ abstract class LinkitBrowserTestBase extends BrowserTestBase { * * @var array */ - public static $modules = ['linkit', 'linkit_test', 'block']; + protected static $modules = ['linkit', 'linkit_test', 'block']; /** * {@inheritdoc} @@ -38,7 +38,7 @@ abstract class LinkitBrowserTestBase extends BrowserTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->placeBlock('page_title_block'); diff --git a/web/modules/linkit/tests/src/Functional/LinkitUpdateTest.php b/web/modules/linkit/tests/src/Functional/LinkitUpdateTest.php deleted file mode 100644 index 2d44842e3f83ce4f2e5498fe3df2f6431944748d..0000000000000000000000000000000000000000 --- a/web/modules/linkit/tests/src/Functional/LinkitUpdateTest.php +++ /dev/null @@ -1,117 +0,0 @@ -<?php - -namespace Drupal\Tests\linkit\Functional; - -use Drupal\filter\Entity\FilterFormat; -use Drupal\FunctionalTests\Update\UpdatePathTestBase; - -/** - * Tests Linkit upgrade paths. - * - * @group Update - * @group legacy - */ -class LinkitUpdateTest extends UpdatePathTestBase { - - /** - * The config factory service. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * {@inheritdoc} - */ - protected $defaultTheme = 'stark'; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->configFactory = $this->container->get('config.factory'); - } - - /** - * Set database dump files to be used. - */ - protected function setDatabaseDumpFiles() { - $this->databaseDumpFiles = [ - __DIR__ . '/../../../tests/fixtures/update/drupal-8.linkit-enabled.standard.php.gz', - __DIR__ . '/../../../tests/fixtures/update/linkit-additions.php', - ]; - } - - /** - * Tests linkit_update_X. - */ - public function testLinkitUpdate8500() { - $editor = $this->configFactory->get('editor.editor.format_1'); - $this->assertNotEmpty($editor->get('settings.plugins.linkit'), 'We got old linkit settings in the editor configuration.'); - $format_1_linkit_profile = $editor->get('settings.plugins.linkit.linkit_profile'); - - $editor = $this->configFactory->get('editor.editor.format_2'); - $this->assertNotEmpty($editor->get('settings.plugins.linkit'), 'We got old linkit settings in the editor configuration.'); - $format_2_linkit_profile = $editor->get('settings.plugins.linkit.linkit_profile'); - - $editor = $this->configFactory->get('editor.editor.format_3'); - $this->assertNotEmpty($editor->get('settings.plugins.linkit'), 'We got old linkit settings in the editor configuration.'); - $format_3_linkit_profile = $editor->get('settings.plugins.linkit.linkit_profile'); - - $test_profile = $this->configFactory->get('linkit.linkit_profile.test_profile'); - $this->assertNotNull($test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.result_description'), 'Profile have result_description'); - $this->assertNotNull($test_profile->get('third_party_settings.imce.use'), 'Profile have imce use'); - $this->assertNotNull($test_profile->get('third_party_settings.imce.scheme'), 'Profile have imce scheme'); - - $this->runUpdates(); - - $test_profile = $this->configFactory->get('linkit.linkit_profile.test_profile'); - $this->assertEquals(NULL, $test_profile->get('attributes'), 'Attributes are deleted from the profile.'); - $this->assertEquals('canonical', $test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.substitution_type'), 'Content matcher has a substitution type of canonical.'); - $this->assertEquals('file', $test_profile->get('matchers.b8d6d672-6377-493f-b492-3cc69511cf17.settings.substitution_type'), 'File matcher has a substitution type of file.'); - $this->assertNull($test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.result_description'), 'Profile does not have result_description'); - $this->assertNotNull($test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.metadata'), 'Profile have metadata'); - $this->assertNull($test_profile->get('third_party_settings.imce.use'), 'Profile does not have imce use'); - $this->assertNull($test_profile->get('third_party_settings.imce.scheme'), 'Profile does not have imce scheme'); - - $editor = $this->configFactory->get('editor.editor.format_1'); - $this->assertNull($editor->get('settings.plugins.linkit'), 'Old linkit settings in the editor configuration is removed.'); - $this->assertEquals($editor->get('settings.toolbar.rows.0.1.items.0'), 'DrupalLink', 'Drupal link plugin is in the toolbar.'); - $this->assertNotEquals($editor->get('settings.toolbar.rows.0.1.items.1'), 'Linkit', 'Linkit plugin is removed from the toolbar.'); - $this->assertNotEmpty($editor->get('settings.plugins.drupallink.linkit_enabled'), 'Drupal link plugin has linkit enabled.'); - $this->assertEquals($editor->get('settings.plugins.drupallink.linkit_profile'), $format_1_linkit_profile, 'Drupal link plugin uses the same profile as the old linkit plugin.'); - - $editor = $this->configFactory->get('editor.editor.format_2'); - $this->assertNull($editor->get('settings.plugins.linkit'), 'Old linkit settings in the editor configuration is removed.'); - $this->assertEquals($editor->get('settings.toolbar.rows.0.1.items.0'), 'DrupalLink', 'Drupal link plugin is in the toolbar.'); - $this->assertNotEmpty($editor->get('settings.plugins.drupallink.linkit_enabled'), 'Drupal link plugin has linkit enabled.'); - $this->assertEquals($editor->get('settings.plugins.drupallink.linkit_profile'), $format_2_linkit_profile, 'Drupal link plugin uses the same profile as the old linkit plugin.'); - - $editor = $this->configFactory->get('editor.editor.format_3'); - $this->assertNull($editor->get('settings.plugins.linkit'), 'Old linkit settings in the editor configuration is removed.'); - $this->assertEquals($editor->get('settings.toolbar.rows.0.0.items.0'), 'DrupalLink', 'Drupal link plugin is in the toolbar.'); - $this->assertNotEmpty($editor->get('settings.plugins.drupallink.linkit_enabled'), 'Drupal link plugin has linkit enabled.'); - $this->assertEquals($editor->get('settings.plugins.drupallink.linkit_profile'), $format_3_linkit_profile, 'Drupal link plugin uses the same profile as the old linkit plugin.'); - - $format = $this->configFactory->get('filter.format.format_1'); - $this->assertNotNull($format->get('filters.linkit'), 'Linkit filter is enabled.'); - $this->assertTrue($format->get('filters.linkit.weight') < $format->get('filters.filter_html.weight'), 'Linkit filter is running before filter_html.'); - - $format = $this->configFactory->get('filter.format.format_2'); - $this->assertNotNull($format->get('filters.linkit'), 'Linkit filter is enabled.'); - - $format = $this->configFactory->get('filter.format.format_3'); - $this->assertNotNull($format->get('filters.linkit'), 'Linkit filter is enabled.'); - - $htmlRestrictions = FilterFormat::load('format_1')->getHtmlRestrictions(); - $this->assertArrayHasKey("data-entity-type", $htmlRestrictions['allowed']['a']); - $this->assertArrayHasKey("data-entity-uuid", $htmlRestrictions['allowed']['a']); - $this->assertArrayHasKey("data-entity-substitution", $htmlRestrictions['allowed']['a']); - - $htmlRestrictions = FilterFormat::load('format_3')->getHtmlRestrictions(); - $this->assertArrayHasKey("data-entity-type", $htmlRestrictions['allowed']['a']); - $this->assertArrayHasKey("data-entity-uuid", $htmlRestrictions['allowed']['a']); - } - -} diff --git a/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php b/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php index 2db6653c28398d551170a4988b4f957b29cd451a..07337440c8adec76a4f8ff27dff4711fbe6973a5 100644 --- a/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php +++ b/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php @@ -34,7 +34,7 @@ class MatcherAdminTest extends LinkitBrowserTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->manager = $this->container->get('plugin.manager.linkit.matcher'); diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php index b5a591072c886fe94055a6e26e368bd6630aa3bd..cbcb9f14e10a07aff33e828af16fe415d3a6f151 100644 --- a/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php +++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\linkit\FunctionalJavascript; +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Url; use Drupal\entity_test\Entity\EntityTestMul; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -21,7 +23,7 @@ class LinkFieldTest extends WebDriverTestBase { /** * {@inheritdoc} */ - public static $modules = [ + protected static $modules = [ 'node', 'language', 'field_ui', @@ -33,7 +35,7 @@ class LinkFieldTest extends WebDriverTestBase { /** * {@inheritdoc} */ - protected $defaultTheme = 'classy'; + protected $defaultTheme = 'stark'; /** * A linkit profile. @@ -45,7 +47,7 @@ class LinkFieldTest extends WebDriverTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entityDisplayRepository */ @@ -87,6 +89,10 @@ protected function setUp() { $entityDisplayRepository->getViewDisplay('node', 'page', 'default') ->setComponent('field_test_link', [ 'type' => 'linkit', + 'settings' => [ + 'rel' => 'nofollow', + 'target' => '_blank', + ], ]) ->save(); @@ -128,7 +134,7 @@ public function testLinkFieldWidgetAndFormatter() { $autocomplete_results_wrapper = $assert_session->elementExists('css', 'ul.linkit-ui-autocomplete'); $this->assertTrue($autocomplete_results_wrapper->isVisible()); $result_description = $assert_session->elementExists('css', 'li.linkit-result-line .linkit-result-line--description', $autocomplete_results_wrapper); - $this->assertEquals('Linkit could not find any suggestions. This URL will be used as is.', $result_description->getText()); + $this->assertEquals('No content suggestions found. This URL will be used as is.', $result_description->getText()); // Set the widget to use our profile and have autofill for link text // enabled and try again. @@ -165,13 +171,13 @@ public function testLinkFieldWidgetAndFormatter() { // Check that we are viewing the node, and the formatter displays what we // expect. - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $this->assertEquals('Foo', $link_element->getText()); - $href_value = $link_element->getAttribute('href'); - $this->assertContains("/entity_test_mul/manage/{$entity->id()}", $href_value); + $assert_session->linkByHrefExists("/entity_test_mul/manage/{$entity->id()}"); + $assert_session->elementAttributeContains('xpath', '//article/div/div/div[2]/a', 'href', "/entity_test_mul/manage/{$entity->id()}"); + $assert_session->elementAttributeContains('xpath', '//article/div/div/div[2]/a', 'rel', "nofollow"); + $assert_session->elementAttributeContains('xpath', '//article/div/div/div[2]/a', 'target', "_blank"); + $assert_session->linkExists('Foo'); - // Test internal entity targets with anchors. + // Test internal entity targets with anchors and query parameters. /** @var \Drupal\Core\Entity\EntityInterface $entity */ $entity2 = EntityTestMul::create(['name' => 'Anchored Entity']); $entity2->save(); @@ -196,7 +202,7 @@ public function testLinkFieldWidgetAndFormatter() { $this->assertEquals('Anchored Entity', $title_input->getValue()); // Add an anchor to the URL field. - $url_input->setValue($entity2->toUrl()->toString() . '#with-anchor'); + $url_input->setValue($entity2->toUrl()->toString() . '#with-anchor?search=1'); // Give the node a title and save the page. $page->fillField('title[0][value]', 'Host test node 2'); @@ -205,11 +211,13 @@ public function testLinkFieldWidgetAndFormatter() { // Check that we are viewing the node, and the formatter displays what we // expect. - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $this->assertEquals('Anchored Entity', $link_element->getText()); - $href_value = $link_element->getAttribute('href'); - $this->assertContains("/entity_test_mul/manage/{$entity2->id()}#with-anchor", $href_value); + $assert_session->linkByHrefExists("/entity_test_mul/manage/{$entity2->id()}#with-anchor?search=1"); + $assert_session->linkExists('Anchored Entity'); + + // Verify anchor persists when visiting the edit form. + $this->drupalGet('node/2/edit'); + $url_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][uri]"]', $widget_wrapper); + $this->assertEquals($entity2->toUrl()->toString() . '#with-anchor?search=1', $url_input->getValue()); // Test external URLs. $this->drupalGet('node/add/page'); @@ -222,7 +230,7 @@ public function testLinkFieldWidgetAndFormatter() { $autocomplete_results_wrapper = $assert_session->elementExists('css', 'ul.linkit-ui-autocomplete'); $this->assertTrue($autocomplete_results_wrapper->isVisible()); $result_description = $assert_session->elementExists('css', 'li.linkit-result-line .linkit-result-line--description', $autocomplete_results_wrapper); - $this->assertEquals('Linkit could not find any suggestions. This URL will be used as is.', $result_description->getText()); + $this->assertEquals('No content suggestions found. This URL will be used as is.', $result_description->getText()); $first_result = $assert_session->elementExists('css', 'ul.linkit-ui-autocomplete li.linkit-result-line span.linkit-result-line--title'); $first_result->click(); $assert_session->assertWaitOnAjaxRequest(); @@ -238,11 +246,8 @@ public function testLinkFieldWidgetAndFormatter() { // Check that we are viewing the node, and the formatter displays what we // expect. - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $this->assertEquals('This is google', $link_element->getText()); - $href_value = $link_element->getAttribute('href'); - $this->assertContains('https://google.com#foobar', $href_value); + $assert_session->linkByHrefExists('https://google.com#foobar'); + $assert_session->linkExists('This is google'); // Test that it is possible to add just the anchor. $this->drupalGet('node/add/page'); @@ -256,11 +261,8 @@ public function testLinkFieldWidgetAndFormatter() { $page->fillField('title[0][value]', 'Host test node 3.5'); $page->pressButton('Save'); $assert_session->pageTextContains('Host test node 3.5 has been created'); - - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $href_value = $link_element->getAttribute('href'); - $this->assertEquals('#foobar', $href_value); + $assert_session->linkByHrefExists('#foobar'); + $assert_session->linkExists('Just a fragment'); // Test emails. $this->drupalGet('node/add/page'); @@ -280,7 +282,7 @@ public function testLinkFieldWidgetAndFormatter() { $this->assertEquals('mailto:' . $email, $url_input->getValue()); // Check that the title was populated automatically. $title_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][title]"]', $widget_wrapper); - $this->assertEquals($email, $title_input->getValue()); + $this->assertEquals((string) new FormattableMarkup('E-mail @email', ['@email' => $email]), $title_input->getValue()); // Give the node a title and save the page. $page->fillField('title[0][value]', 'Host test node 4'); @@ -289,11 +291,8 @@ public function testLinkFieldWidgetAndFormatter() { // Check that we are viewing the node, and the formatter displays what we // expect. - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $this->assertEquals($email, $link_element->getText()); - $href_value = $link_element->getAttribute('href'); - $this->assertContains('mailto:' . $email, $href_value); + $assert_session->linkByHrefExists('mailto:' . $email); + $assert_session->linkExists((string) new FormattableMarkup('E-mail @email', ['@email' => $email])); // Test internal host. $this->drupalGet('node/add/page'); @@ -321,11 +320,8 @@ public function testLinkFieldWidgetAndFormatter() { // Check that we are viewing the node, and the formatter displays what we // expect Should display the relative url without the host. - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $this->assertEquals($url, $link_element->getText()); - $href_value = $link_element->getAttribute('href'); - $this->assertContains('/node/1', $href_value); + $assert_session->linkByHrefExists('/node/1'); + $assert_session->linkExists($url); // Test front page. $this->drupalGet('node/add/page'); @@ -341,7 +337,7 @@ public function testLinkFieldWidgetAndFormatter() { $url_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][uri]"]', $widget_wrapper); - $this->assertEquals('/', $url_input->getValue()); + $this->assertEquals(Url::fromRoute('<front>')->toString(), $url_input->getValue()); // Check that the title was populated automatically. $title_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][title]"]', $widget_wrapper); $this->assertEquals('Front page', $title_input->getValue()); @@ -351,11 +347,8 @@ public function testLinkFieldWidgetAndFormatter() { $page->pressButton('Save'); $assert_session->pageTextContains('Host test node 6 has been created'); - $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link'); - $link_element = $assert_session->elementExists('css', 'a', $field_wrapper); - $this->assertEquals('Front page', $link_element->getText()); - $href_value = $link_element->getAttribute('href'); - $this->assertContains('/', $href_value); + $assert_session->linkByHrefExists(Url::fromRoute('<front>')->toString()); + $assert_session->linkExists('Front page'); // Test invalid input. foreach (['foo:0123456', ':', '123:bar'] as $key => $invalid_string) { diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php new file mode 100644 index 0000000000000000000000000000000000000000..0cb9865987fee634df957663ef3ee74228f2be7d --- /dev/null +++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php @@ -0,0 +1,228 @@ +<?php + +namespace Drupal\Tests\linkit\FunctionalJavascript; + +use Drupal\ckeditor5\Plugin\Editor\CKEditor5; +use Drupal\editor\Entity\Editor; +use Drupal\entity_test\Entity\EntityTestMul; +use Drupal\filter\Entity\FilterFormat; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\linkit\MatcherInterface; +use Drupal\linkit\Tests\ProfileCreationTrait; +use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * Tests the Linkit extensions to the CKEditor 5 Link plugin. + * + * @group linkit + * @group ckeditor5 + */ +class LinkitDialogCKEditor5Test extends WebDriverTestBase { + + use ProfileCreationTrait; + use CKEditor5TestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'node', + 'ckeditor5', + 'filter', + 'linkit', + 'entity_test', + 'language', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * An instance of the "CKEditor" text editor plugin. + * + * @var \Drupal\ckeditor\Plugin\Editor\CKEditor + */ + protected $ckeditor; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $matcher_manager = $this->container->get('plugin.manager.linkit.matcher'); + $linkit_profile = $this->createProfile(); + $plugin = $matcher_manager->createInstance('entity:entity_test_mul'); + assert($plugin instanceof MatcherInterface); + $linkit_profile->addMatcher($plugin->getConfiguration()); + $linkit_profile->save(); + + // Create text format, associate CKEditor 5, validate. + FilterFormat::create([ + 'format' => 'test_format', + 'name' => 'Test format', + 'filters' => [ + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ + 'allowed_html' => '<p> <br> <a href data-entity-type data-entity-uuid data-entity-substitution>', + ], + ], + ], + ])->save(); + Editor::create([ + 'format' => 'test_format', + 'editor' => 'ckeditor5', + 'settings' => [ + 'toolbar' => [ + 'items' => [ + 'link', + ], + ], + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => $linkit_profile->id(), + ], + ], + ], + ])->save(); + $this->assertSame([], array_map( + function (ConstraintViolation $v) { + return (string) $v->getMessage(); + }, + iterator_to_array(CKEditor5::validatePair( + Editor::load('test_format'), + FilterFormat::load('test_format') + )) + )); + + // Create a node type for testing. + $this->drupalCreateContentType(['type' => 'page']); + + $account = $this->drupalCreateUser([ + 'create page content', + 'use text format test_format', + 'view test entity', + ]); + + $this->drupalLogin($account); + } + + /** + * Test the link dialog. + */ + public function testLinkDialog() { + $session = $this->getSession(); + $assert_session = $this->assertSession(); + $page = $session->getPage(); + + // Adds additional languages. + $langcodes = ['sv', 'da', 'fi']; + foreach ($langcodes as $langcode) { + ConfigurableLanguage::createFromLangcode($langcode)->save(); + } + + // Create a test entity. + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = EntityTestMul::create(['name' => 'Foo']); + $entity->save(); + + $this->drupalGet('node/add/page'); + $this->waitForEditor(); + $this->pressEditorButton('Link'); + + // Find the href field. + $balloon = $this->assertVisibleBalloon('.ck-link-form'); + $autocomplete_field = $balloon->find('css', '.ck-input-text'); + + // Make sure all fields are empty. + $this->assertEmpty($autocomplete_field->getValue(), 'Autocomplete field is empty.'); + + // Make sure the autocomplete result container is hidden. + $autocomplete_container = $assert_session->elementExists('css', '.ck-link-form .linkit-ui-autocomplete'); + $this->assertFalse($autocomplete_container->isVisible()); + + // Trigger a keydown event to activate a autocomplete search. + $autocomplete_field->setValue('f'); + $this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' '); + $this->assertTrue($this->getSession()->wait(5000, "document.querySelectorAll('.linkit-result-line.ui-menu-item').length > 0")); + + // Make sure the autocomplete result container is visible. + $this->assertTrue($autocomplete_container->isVisible()); + + // Make sure the autocomplete result container is excluded from CKEditor5 CSS resets: + $assert_session->elementExists('css', '.ck-link-form .linkit-ui-autocomplete.ck-reset_all-excluded'); + + // Find all the autocomplete results. + $results = $page->findAll('css', '.linkit-result-line.ui-menu-item'); + $this->assertCount(1, $results); + + // Find the first result and click it. + $results[0]->click(); + + // Make sure the linkit field field is populated with the test entity's URL. + $expected_url = base_path() . 'entity_test_mul/manage/1'; + $this->assertSame($expected_url, $autocomplete_field->getValue()); + $balloon->pressButton('Save'); + // Assert balloon was closed by pressing its "Save" button. + $this->assertFalse($page->find('css', '.ck-balloon-panel')->isVisible()); + + // Make sure all attributes are populated. + $linkit_link = $assert_session->waitForElementVisible('css', '.ck-content a'); + $this->assertNotNull($linkit_link); + $this->assertSame($expected_url, $linkit_link->getAttribute('href')); + $this->assertSame('entity_test_mul', $linkit_link->getAttribute('data-entity-type')); + $this->assertSame($entity->uuid(), $linkit_link->getAttribute('data-entity-uuid')); + $this->assertSame('canonical', $linkit_link->getAttribute('data-entity-substitution')); + + // Open the edit link dialog by moving selection to the link, verifying the + // "Link" button is off before and on after, and then pressing that button. + $this->assertFalse($this->getEditorButton('Link')->hasClass('ck-on')); + $this->selectTextInsideElement('a'); + $this->assertTrue($this->getEditorButton('Link')->hasClass('ck-on')); + $this->pressEditorButton('Link'); + $this->assertVisibleBalloon('.ck-link-actions'); + $edit_button = $this->getBalloonButton('Edit link'); + $edit_button->click(); + $link_edit_balloon = $this->assertVisibleBalloon('.ck-link-form'); + $autocomplete_field = $link_edit_balloon->find('css', '.ck-input-text'); + $this->assertSame($expected_url, $autocomplete_field->getValue()); + // Click to trigger the reset of the the autocomplete status. + $autocomplete_field->click(); + // Enter a URL and verify that no link suggestions are found. + $autocomplete_field->setValue('http://example.com'); + $autocomplete_field->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->waitForElementVisible('css', '.linkit-result-line.ui-menu-item'); + $results = $page->findAll('css', '.linkit-result-line.ui-menu-item'); + $this->assertCount(1, $results); + $this->assertSame('http://example.com', $results[0]->find('css', '.linkit-result-line--title')->getText()); + $this->assertSame('No content suggestions found. This URL will be used as is.', $results[0]->find('css', '.linkit-result-line--description')->getText()); + // Decline the autocomplete suggestion. + $link_edit_balloon->pressButton('Cancel'); + // Accept the link as-is. + $link_edit_balloon->pressButton('Save'); + $this->assertTrue($assert_session->waitForElementRemoved('css', '.ck-button-save')); + // Assert balloon is still visible, but now it's again the link actions one. + $this->assertVisibleBalloon('.ck-link-actions'); + // Assert balloon can be closed by clicking elsewhere in the editor. + $page->find('css', '.ck-editor__editable')->click(); + $this->assertFalse($page->find('css', '.ck-balloon-panel')->isVisible()); + + $changed_link = $assert_session->waitForElementVisible('css', '.ck-content [href="http://example.com"]'); + $this->assertNotNull($changed_link); + foreach ([ + 'data-entity-type', + 'data-entity-uuid', + 'data-entity-substitution', + ] as $attribute_name) { + $this->assertFalse($changed_link->hasAttribute($attribute_name), "Link should no longer have $attribute_name"); + } + } + +} diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php index 57ca481d2ba9db355b094c665c5453521ff65d77..b53cceaea96789f2fd2e28f2a9a631e2cdc8a8eb 100644 --- a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php +++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php @@ -27,9 +27,8 @@ class LinkitDialogTest extends WebDriverTestBase { * * @var array */ - public static $modules = [ + protected static $modules = [ 'node', - 'ckeditor', 'filter', 'linkit', 'entity_test', @@ -58,9 +57,15 @@ class LinkitDialogTest extends WebDriverTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); + if (!in_array('ckeditor', $this->container->get('extension.list.module')->reset()->getList(), TRUE)) { + $this->markTestSkipped('CKEditor 4 module not available to install, skipping test.'); + } + $this->container->get('module_installer')->install(['ckeditor']); + $this->container = \Drupal::getContainer(); + $matcherManager = $this->container->get('plugin.manager.linkit.matcher'); /** @var \Drupal\linkit\MatcherInterface $plugin */ @@ -160,6 +165,9 @@ public function testLinkDialog() { // Wait for the form to load. $web_assert->assertWaitOnAjaxRequest(); + // The dialog is not tall enough to allow the autocomplete to be visible. + $this->getSession()->executeScript("document.getElementById('drupal-modal').style.height = '200px';"); + // Find the href field. $href_field = $page->findField('attributes[href]'); @@ -196,6 +204,9 @@ public function testLinkDialog() { // Find the first result and click it. $page->find('xpath', '//li[contains(@class, "linkit-result-line") and contains(@class, "ui-menu-item")][1]')->click(); + // Make sure the linkit field field is populated with the node url. + $this->assertEquals($entity->toUrl()->toString(), $href_field->getValue(), 'The href field is populated with the node url.'); + // Make sure all other fields are populated. $this->assertEqualsWithJs('attributes[data-entity-type]', $entity->getEntityTypeId()); $this->assertEqualsWithJs('attributes[data-entity-uuid]', $entity->uuid()); diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php index 3abe5a4d7014eb17d820194197f7b99a191c6c49..f7f4bd27380e2592d5a890ee581c0db698e85aba 100644 --- a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php +++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php @@ -16,7 +16,7 @@ class LinkitFormatAdminTest extends WebDriverTestBase { * * @var array */ - public static $modules = ['editor', 'filter', 'linkit']; + protected static $modules = ['editor', 'filter', 'linkit']; /** * {@inheritdoc} @@ -26,7 +26,7 @@ class LinkitFormatAdminTest extends WebDriverTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $account = $this->drupalCreateUser([ @@ -49,15 +49,15 @@ public function testToggleLinkitFilter() { $page->findField('filters[filter_html][status]')->check(); $javascript = "(function (){ return jQuery('p.editor-update-message > strong').text(); })()"; - $this->assertNotContains('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript)); + $this->assertStringNotContainsString('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript)); // Enable the 'Linkit filter' filter. $page->findField('filters[linkit][status]')->check(); - $this->assertContains('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript)); + $this->assertStringContainsString('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript)); // Disable the 'Linkit filter' filter. $page->findField('filters[linkit][status]')->uncheck(); - $this->assertNotContains('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript)); + $this->assertStringNotContainsString('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript)); } } diff --git a/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php b/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php index 24ca82273e5612f3676df11cb41f4876ea16e0ac..3e77f69e0e6f75e11d3e7ba1af366a9d79c557a1 100644 --- a/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php +++ b/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php @@ -29,7 +29,7 @@ trait AssertLinkitFilterTrait { protected function assertLinkitFilter(EntityInterface $entity, $langcode = LanguageInterface::LANGCODE_SITE_DEFAULT) { if ($entity->getEntityTypeId() === "file") { /** @var \Drupal\file\Entity\File $entity */ - $href = file_create_url($entity->getFileUri()); + $href = \Drupal::service('file_url_generator')->generateString($entity->getFileUri()); } else { $href = $entity->toUrl()->toString(); @@ -38,6 +38,8 @@ protected function assertLinkitFilter(EntityInterface $entity, $langcode = Langu $input = '<a data-entity-type="' . $entity->getEntityTypeId() . '" data-entity-uuid="' . $entity->uuid() . '">Link text</a>'; $expected = '<a data-entity-type="' . $entity->getEntityTypeId() . '" data-entity-uuid="' . $entity->uuid() . '" href="' . $href . '">Link text</a>'; $this->assertSame($expected, $this->process($input, $langcode)->getProcessedText()); + $canonical_url_aka_not_path_alias = '/entity_test_mul/manage/1'; + $this->assertStringNotContainsString($canonical_url_aka_not_path_alias, $this->process($input, $langcode)->getProcessedText()); } /** @@ -51,7 +53,7 @@ protected function assertLinkitFilter(EntityInterface $entity, $langcode = Langu protected function assertLinkitFilterWithTitle(EntityInterface $entity, $langcode = LanguageInterface::LANGCODE_SITE_DEFAULT) { if ($entity->getEntityTypeId() === "file") { /** @var \Drupal\file\Entity\File $entity */ - $href = file_create_url($entity->getFileUri()); + $href = \Drupal::service('file_url_generator')->generateString($entity->getFileUri()); } else { $href = $entity->toUrl()->toString(); diff --git a/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathCompletenessTest.php b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathCompletenessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8babece4ce356d1aa37fb020fb55e4b9bb2dbccc --- /dev/null +++ b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathCompletenessTest.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\linkit\Kernel\CKEditor4To5Upgrade; + +use Drupal\Tests\ckeditor5\Kernel\CKEditor4to5UpgradeCompletenessTest as Real; +use Drupal\KernelTests\KernelTestBase; + +if (class_exists(Real::class)) { + class CKEditor4to5UpgradeCompletenessTest extends Real { } +} else { + class CKEditor4to5UpgradeCompletenessTest extends KernelTestBase { + public function testImpossible() { + $this->markTestSkipped(); + } + } +} + +/** + * @covers \Drupal\linkit\Plugin\CKEditor4To5Upgrade\Linkit + * @group linkit + * @group ckeditor5 + * @internal + * @requires module ckeditor5 + */ +class UpgradePathCompletenessTest extends CKEditor4to5UpgradeCompletenessTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['linkit']; + +} diff --git a/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathTest.php b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathTest.php new file mode 100644 index 0000000000000000000000000000000000000000..eff4f83c8ef4e63a28f53e08718e935860fa2727 --- /dev/null +++ b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathTest.php @@ -0,0 +1,188 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\linkit\Kernel\CKEditor4To5Upgrade; + +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; +use Drupal\Tests\ckeditor5\Kernel\SmartDefaultSettingsTest; + +/** + * @covers \Drupal\linkit\Plugin\CKEditor4To5Upgrade\Linkit + * @group linkit + * @group ckeditor5 + * @requires module ckeditor5 + * @internal + */ +class UpgradePathTest extends SmartDefaultSettingsTest { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'linkit', + // Because modules/linkit/config/optional/linkit.linkit_profile.default.yml + // will only then get installed. + 'node', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig(['linkit']); + + $filter_config = [ + 'filter_html' => [ + 'status' => 1, + 'settings' => [ + 'allowed_html' => '<p> <br> <strong> <a href>', + ], + ], + ]; + FilterFormat::create([ + 'format' => 'linkit_disabled', + 'name' => 'Linkit disabled', + 'filters' => $filter_config, + ])->setSyncing(TRUE)->save(); + FilterFormat::create([ + 'format' => 'linkit_enabled_misconfigured_format', + 'name' => 'Linkit enabled on a misconfigured format', + 'filters' => $filter_config, + ])->setSyncing(TRUE)->save(); + FilterFormat::create([ + 'format' => 'linkit_enabled', + 'name' => 'Linkit enabled on a well-configured format', + 'filters' => [ + 'filter_html' => [ + 'status' => 1, + 'settings' => [ + 'allowed_html' => '<p> <br> <strong> <a href data-entity-type data-entity-uuid data-entity-substitution>', + ], + ], + ], + ])->setSyncing(TRUE)->save(); + + $generate_editor_settings = function (array $linkit_cke4_settings) { + return [ + 'toolbar' => [ + 'rows' => [ + 0 => [ + [ + 'name' => 'Basic Formatting', + 'items' => [ + 'Bold', + 'Format', + 'DrupalLink' + ], + ], + ], + ], + ], + 'plugins' => [ + 'drupallink' => $linkit_cke4_settings, + ], + ]; + }; + + Editor::create([ + 'format' => 'linkit_disabled', + 'editor' => 'ckeditor', + 'settings' => $generate_editor_settings([ + 'linkit_enabled' => FALSE, + 'linkit_profile' => '', + ]), + ])->setSyncing(TRUE)->save(); + Editor::create([ + 'format' => 'linkit_enabled_misconfigured_format', + 'editor' => 'ckeditor', + 'settings' => $generate_editor_settings([ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'default', + ]), + ])->setSyncing(TRUE)->save(); + Editor::create([ + 'format' => 'linkit_enabled', + 'editor' => 'ckeditor', + 'settings' => $generate_editor_settings([ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'default', + ]), + ])->setSyncing(TRUE)->save(); + } + + /** + * {@inheritdoc} + */ + public function provider() { + $expected_ckeditor5_toolbar = [ + 'items' => [ + 'bold', + 'link', + ], + ]; + + yield "linkit disabled" => [ + 'format_id' => 'linkit_disabled', + 'filters_to_drop' => [], + 'expected_ckeditor5_settings' => [ + 'toolbar' => $expected_ckeditor5_toolbar, + 'plugins' => [], + ], + 'expected_superset' => '', + 'expected_fundamental_compatibility_violations' => [], + 'expected_db_logs' => [], + 'expected_messages' => [], + ]; + + yield "linkit enabled on a misconfigured text format" => [ + 'format_id' => 'linkit_enabled_misconfigured_format', + 'filters_to_drop' => [], + 'expected_ckeditor5_settings' => [ + 'toolbar' => $expected_ckeditor5_toolbar, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'default', + ], + ], + ], + 'expected_superset' => '<a data-entity-type data-entity-uuid data-entity-substitution>', + 'expected_fundamental_compatibility_violations' => [], + 'expected_db_logs' => [], + 'expected_messages' => [ + 'warning' => [ + 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: These attributes: <em class="placeholder"> data-entity-type (for <a>), data-entity-uuid (for <a>), data-entity-substitution (for <a>)</em>; Additional details are available in your logs.', + ], + ], + ]; + + yield "linkit enabled on a well-configured text format" => [ + 'format_id' => 'linkit_enabled', + 'filters_to_drop' => [], + 'expected_ckeditor5_settings' => [ + 'toolbar' => $expected_ckeditor5_toolbar, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'default', + ], + ], + ], + 'expected_superset' => '', + 'expected_fundamental_compatibility_violations' => [], + 'expected_db_logs' => [], + 'expected_messages' => [], + ]; + + // Verify that none of the core test cases are broken; especially important + // for Linkit since it extends the behavior of Drupal core. + foreach (parent::provider() as $label => $case) { + yield $label => $case; + } + } + +} diff --git a/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php b/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php index cf3832a7e891e087fb24ca44c74e3381ebbeab87..c8c4c5539284052b2a752f1186401c336ef1ed58 100644 --- a/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php +++ b/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php @@ -12,7 +12,7 @@ class EntityMatcherDeriverTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['block', 'block_content', 'node', 'field']; + protected static $modules = ['block', 'block_content', 'node', 'field']; /** * The matcher manager. @@ -24,7 +24,7 @@ class EntityMatcherDeriverTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->installConfig(['block_content']); diff --git a/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php b/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php index b4498e55311ffc7087234c2f461179b23ee4c80f..a5595713ad91791119e8e4877857b6f9ffb37d76 100644 --- a/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php +++ b/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\linkit\Kernel; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; use Drupal\entity_test\Entity\EntityTest; @@ -23,7 +24,7 @@ class LinkitAutocompleteTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['entity_test', 'language']; + protected static $modules = ['entity_test', 'language']; /** * The linkit profile. @@ -49,7 +50,7 @@ class LinkitAutocompleteTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Create user 1 who has special permissions. @@ -109,7 +110,7 @@ public function testAutocompletionEmail() { $email = 'drupal@example.com'; $data = $this->getAutocompleteResult($email); - $this->assertSame($email, $data[0]['label'], 'Autocomplete returned email suggestion.'); + $this->assertSame((string) new FormattableMarkup('E-mail @email', ['@email' => $email]), $data[0]['label'], 'Autocomplete returned email suggestion.'); $this->assertSame('mailto:' . $email, $data[0]['path'], 'Autocomplete returned email suggestion with an mailto href.'); } diff --git a/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php b/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php index 42504fcdb6456686f405c5f45d2c641fe59e0667..cd17291d83c15c08339725f594f7cb81c5b9c72b 100644 --- a/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php +++ b/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php @@ -38,16 +38,20 @@ class LinkitEditorLinkDialogTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['editor', 'ckeditor', 'entity_test']; + protected static $modules = ['editor', 'entity_test']; /** * Sets up the test. */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); + if (!in_array('ckeditor', $this->container->get('extension.list.module')->reset()->getList(), TRUE)) { + $this->markTestSkipped('CKEditor 4 module not available to install, skipping test.'); + } + $this->enableModules(['ckeditor']); + $this->installEntitySchema('entity_test'); - $this->installSchema('system', ['key_value_expire']); // Create a profile. $this->linkitProfile = $this->createProfile(); @@ -75,14 +79,11 @@ protected function setUp() { 'format' => 'filtered_html', 'editor' => 'ckeditor', ]); - $this->editor->setSettings([ - 'plugins' => [ - 'drupallink' => [ - 'linkit_enabled' => TRUE, - 'linkit_profile' => $this->linkitProfile->id(), - ], - ], - ]); + $this->editor->save(); + $settings = $this->editor->getSettings(); + $settings['plugins']['drupallink']['linkit_enabled'] = TRUE; + $settings['plugins']['drupallink']['linkit_profile'] = $this->linkitProfile->id(); + $this->editor->setSettings($settings); $this->editor->save(); } @@ -128,14 +129,16 @@ public function testAdd() { $form_state->setValue(['attributes', 'href'], 'https://example.com/'); $form_state->setValue('href_dirty_check', ''); - $form_state->setValue(['attributes', 'data-entity-type'], $entity->getEntityTypeId()); + $form_state->setValue(['attributes', 'data-entity-type'], $this->randomString()); $form_state->setValue(['attributes', 'data-entity-uuid'], $this->randomString()); $form_state->setValue(['attributes', 'data-entity-substitution'], $this->randomString()); $form_builder->submitForm($form_object, $form_state); $this->assertEmpty($form_state->getValue(['attributes', 'data-entity-type'])); $this->assertEmpty($form_state->getValue(['attributes', 'data-entity-uuid'])); - $this->assertEmpty($form_state->getValue(['attributes', 'data-entity-substitution'])); - + $this->assertEmpty($form_state->getValue([ + 'attributes', + 'data-entity-substitution', + ])); $entity_url = $entity->toUrl('canonical', ['path_processing' => FALSE])->toString(); $form_state->setValue(['attributes', 'href'], $entity_url); $form_state->setValue('href_dirty_check', $entity_url); @@ -144,9 +147,18 @@ public function testAdd() { $form_state->setValue(['attributes', 'data-entity-substitution'], SubstitutionManagerInterface::DEFAULT_SUBSTITUTION); $form_builder->submitForm($form_object, $form_state); - $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue(['attributes', 'data-entity-type']), 'Attribute "data-entity-type" exists and has the correct value.'); - $this->assertEquals($entity->uuid(), $form_state->getValue(['attributes', 'data-entity-uuid']), 'Attribute "data-entity-uuid" exists and has the correct value.'); - $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue(['attributes', 'data-entity-substitution']), 'Attribute "data-entity-substitution" exists and has the correct value.'); + $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue([ + 'attributes', + 'data-entity-type', + ]), 'Attribute "data-entity-type" exists and has the correct value.'); + $this->assertEquals($entity->uuid(), $form_state->getValue([ + 'attributes', + 'data-entity-uuid', + ]), 'Attribute "data-entity-uuid" exists and has the correct value.'); + $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue([ + 'attributes', + 'data-entity-substitution', + ]), 'Attribute "data-entity-substitution" exists and has the correct value.'); } /** @@ -194,9 +206,18 @@ public function testEditWithDataAttributes() { $this->assertEquals('linkit.autocomplete', $form['attributes']['href']['#autocomplete_route_name'], 'Linkit is enabled on the href field.'); $this->assertEquals($entity_url, $form['attributes']['href']['#default_value'], 'The href field has the url as default value.'); - $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue(['attributes', 'data-entity-type']), 'Attribute "data-entity-type" exists and has the correct value.'); - $this->assertEquals($entity->uuid(), $form_state->getValue(['attributes', 'data-entity-uuid']), 'Attribute "data-entity-uuid" exists and has the correct value.'); - $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue(['attributes', 'data-entity-substitution']), 'Attribute "data-entity-substitution" exists and has the correct value.'); + $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue([ + 'attributes', + 'data-entity-type', + ]), 'Attribute "data-entity-type" exists and has the correct value.'); + $this->assertEquals($entity->uuid(), $form_state->getValue([ + 'attributes', + 'data-entity-uuid', + ]), 'Attribute "data-entity-uuid" exists and has the correct value.'); + $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue([ + 'attributes', + 'data-entity-substitution', + ]), 'Attribute "data-entity-substitution" exists and has the correct value.'); } /** diff --git a/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php b/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php index 94897b37472e4d330b1d3f80e756f6238bc8187b..149e2c90ca6ea3f46c6aaba3f5c91a0b75348147 100644 --- a/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php +++ b/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php @@ -2,9 +2,11 @@ namespace Drupal\Tests\linkit\Kernel; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\entity_test\Entity\EntityTest; use Drupal\entity_test\Entity\EntityTestMul; use Drupal\file\Entity\File; +use Drupal\file\FileInterface; use Drupal\filter\FilterPluginCollection; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\Traits\Core\PathAliasTestTrait; @@ -26,7 +28,7 @@ class LinkitFilterEntityTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = [ + protected static $modules = [ 'filter', 'entity_test', 'path', @@ -38,7 +40,7 @@ class LinkitFilterEntityTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->installEntitySchema('entity_test'); @@ -56,6 +58,18 @@ protected function setUp() { $this->filter = $bag->get('linkit'); } + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + + // Undo what the parent did, to allow testing path aliases in kernel tests. + $container->getDefinition('path_alias.path_processor') + ->addTag('path_processor_inbound') + ->addTag('path_processor_outbound'); + } + /** * Tests the linkit filter for entities with different access. */ @@ -122,7 +136,7 @@ public function testFilterFileEntity() { 'filename' => 'druplicon.txt', 'uri' => 'public://druplicon.txt', 'filemime' => 'text/plain', - 'status' => FILE_STATUS_PERMANENT, + 'status' => FileInterface::STATUS_PERMANENT, ]); $file->save(); @@ -161,8 +175,8 @@ public function testQueryAndFragments() { // Make sure original query and fragment are preserved. $input = '<a data-entity-type="' . $entity->getEntityTypeId() . '" data-entity-uuid="' . $entity->uuid() . '" href="unimportant/1234?query=string#fragment">Link text</a>'; - $this->assertContains('?query=string', $this->process($input)->getProcessedText()); - $this->assertContains('#fragment', $this->process($input)->getProcessedText()); + $this->assertStringContainsString('?query=string', $this->process($input)->getProcessedText()); + $this->assertStringContainsString('#fragment', $this->process($input)->getProcessedText()); } } diff --git a/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php b/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php index d04a2bc1cba504f41e4654a1910a2809f5e67dd1..b54e917eead93dfd4600e5f52e8b69a576acbbc8 100644 --- a/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php +++ b/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php @@ -16,7 +16,7 @@ abstract class LinkitKernelTestBase extends KernelTestBase { * * @var array */ - public static $modules = [ + protected static $modules = [ 'system', 'user', 'filter', @@ -28,7 +28,7 @@ abstract class LinkitKernelTestBase extends KernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->installSchema('system', 'sequences'); $this->installEntitySchema('user'); diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php index 7d805033712a6bf48211a5f8728d195b9c31fb91..37db55f55587803309cd2117639ef956c7b2bf04 100644 --- a/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php +++ b/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php @@ -17,7 +17,7 @@ class ContactFormMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['contact']; + protected static $modules = ['contact']; /** * The matcher manager. @@ -29,13 +29,13 @@ class ContactFormMatcherTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Create user 1 who has special permissions. $this->createUser(); - \Drupal::currentUser()->setAccount($this->createUser([], ['access site-wide contact form', 'view test entity translations'])); + \Drupal::currentUser()->setAccount($this->createUser([], ['access site-wide contact form'])); $this->manager = $this->container->get('plugin.manager.linkit.matcher'); diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php index 4999aae1ed639fa3f586ea402e0c0d900e0f5e47..1c90f1c1a8515b867c654b1586ec51c5568629de 100644 --- a/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php +++ b/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\linkit\Kernel\Matchers; use Drupal\file\Entity\File; +use Drupal\file\FileInterface; use Drupal\Tests\linkit\Kernel\LinkitKernelTestBase; /** @@ -17,7 +18,7 @@ class FileMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['file_test', 'file']; + protected static $modules = ['file_test', 'file']; /** * The matcher manager. @@ -29,11 +30,10 @@ class FileMatcherTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->installEntitySchema('file'); - $this->installSchema('system', ['key_value_expire']); $this->installSchema('file', ['file_usage']); $this->manager = $this->container->get('plugin.manager.linkit.matcher'); @@ -45,7 +45,7 @@ protected function setUp() { 'filename' => 'image-test.' . $ext, 'uri' => 'public://image-test.' . $ext, 'filemime' => 'text/plain', - 'status' => FILE_STATUS_PERMANENT, + 'status' => FileInterface::STATUS_PERMANENT, ]); $file->save(); } @@ -105,8 +105,8 @@ public function testTermMatcherWidthMetadataTokens() { $suggestions = $suggestionCollection->getSuggestions(); foreach ($suggestions as $suggestion) { - $this->assertNotContains('[file:fid]', $suggestion->getDescription(), 'Raw token "[file:fid]" is not present in the description'); - $this->assertNotContains('[file:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[file:field_with_no_value]" is not present in the description'); + $this->assertStringNotContainsString('[file:fid]', $suggestion->getDescription(), 'Raw token "[file:fid]" is not present in the description'); + $this->assertStringNotContainsString('[file:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[file:field_with_no_value]" is not present in the description'); } } diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php index 1302bdfa99bab71acbf0da1ffb2484f578355921..64013a6e8ac233b01730f31d41f3c083a5c07ca8 100644 --- a/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php +++ b/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\linkit\Kernel\Matchers; use Drupal\file\Entity\File; +use Drupal\file\FileInterface; use Drupal\media\Entity\Media; use Drupal\media\Entity\MediaType; use Drupal\Tests\linkit\Kernel\LinkitKernelTestBase; @@ -19,7 +20,7 @@ class MediaMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['file_test', 'file', 'media', 'image', 'field']; + protected static $modules = ['file_test', 'file', 'media', 'image', 'field']; /** * The matcher manager. @@ -31,13 +32,12 @@ class MediaMatcherTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->installEntitySchema('file'); $this->installEntitySchema('media'); $this->installConfig(['media']); - $this->installSchema('system', ['key_value_expire']); $this->installSchema('file', ['file_usage']); $this->manager = $this->container->get('plugin.manager.linkit.matcher'); @@ -64,7 +64,7 @@ protected function setUp() { 'filename' => 'image-test.' . $ext, 'uri' => 'public://image-test.' . $ext, 'filemime' => 'text/plain', - 'status' => FILE_STATUS_PERMANENT, + 'status' => FileInterface::STATUS_PERMANENT, ]); $file->save(); diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php index 0ab55506edb60546da7e1672e204e020c322fa80..4ee67a880b3eb0033dfdaa0e550ba1d05be4f1fb 100644 --- a/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php +++ b/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php @@ -18,7 +18,7 @@ class NodeMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['field', 'node', 'content_moderation', 'workflows']; + protected static $modules = ['field', 'node', 'content_moderation', 'workflows']; /** * The matcher manager. @@ -30,7 +30,7 @@ class NodeMatcherTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->installEntitySchema('node'); @@ -175,8 +175,8 @@ public function testNodeMatcherWidthMetadataTokens() { $suggestions = $suggestionCollection->getSuggestions(); foreach ($suggestions as $suggestion) { - $this->assertNotContains('[node:nid]', $suggestion->getDescription(), 'Raw token "[node:nid]" is not present in the description'); - $this->assertNotContains('[node:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[node:field_with_no_value]" is not present in the description'); + $this->assertStringNotContainsString('[node:nid]', $suggestion->getDescription(), 'Raw token "[node:nid]" is not present in the description'); + $this->assertStringNotContainsString('[node:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[node:field_with_no_value]" is not present in the description'); } } diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php index 4e04d0fde9bca5eef604cb2762c009546bbd4550..98d70206d0cec3a62201502f4686c0b520c81340 100644 --- a/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php +++ b/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php @@ -20,7 +20,7 @@ class TermMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['taxonomy']; + protected static $modules = ['taxonomy']; /** * The matcher manager. @@ -32,7 +32,7 @@ class TermMatcherTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Create user 1 who has special permissions. @@ -98,8 +98,8 @@ public function testTermMatcherWidthMetadataTokens() { $suggestions = $suggestionCollection->getSuggestions(); foreach ($suggestions as $suggestion) { - $this->assertNotContains('[term:nid]', $suggestion->getDescription(), 'Raw token "[term:nid]" is not present in the description'); - $this->assertNotContains('[term:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[term:field_with_no_value]" is not present in the description'); + $this->assertStringNotContainsString('[term:nid]', $suggestion->getDescription(), 'Raw token "[term:nid]" is not present in the description'); + $this->assertStringNotContainsString('[term:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[term:field_with_no_value]" is not present in the description'); } } diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php index a9d8df5b348122189ba724db46803ba04ba54ade..d2ef802604553d95b8289fe4a668251d4c496b2d 100644 --- a/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php +++ b/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php @@ -22,7 +22,7 @@ class UserMatcherTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Create user 1 who has special permissions. @@ -130,8 +130,8 @@ public function testTermMatcherWidthMetadataTokens() { $suggestions = $suggestionCollection->getSuggestions(); foreach ($suggestions as $suggestion) { - $this->assertNotContains('[user:uid]', $suggestion->getDescription(), 'Raw token "[user:nid]" is not present in the description'); - $this->assertNotContains('[user:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[user:field_with_no_value]" is not present in the description'); + $this->assertStringNotContainsString('[user:uid]', $suggestion->getDescription(), 'Raw token "[user:nid]" is not present in the description'); + $this->assertStringNotContainsString('[user:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[user:field_with_no_value]" is not present in the description'); } } diff --git a/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php b/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php index 0bc6202e6e3bb8549c8fa3f422b997e90273c0c5..32f9ed647c8c97b39e8f5fb7c26017b2b08a4a56 100644 --- a/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php +++ b/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Site\Settings; use Drupal\entity_test\Entity\EntityTest; use Drupal\file\Entity\File; +use Drupal\file\FileInterface; use Drupal\linkit\Plugin\Linkit\Substitution\Canonical as CanonicalSubstitutionPlugin; use Drupal\linkit\Plugin\Linkit\Substitution\File as FileSubstitutionPlugin; use Drupal\linkit\Plugin\Linkit\Substitution\Media as MediaSubstitutionPlugin; @@ -38,7 +39,7 @@ class SubstitutionPluginTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = [ + protected static $modules = [ 'file', 'entity_test', 'media', @@ -50,7 +51,7 @@ class SubstitutionPluginTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->substitutionManager = $this->container->get('plugin.manager.linkit.substitution'); $this->entityTypeManager = $this->container->get('entity_type.manager'); @@ -106,10 +107,10 @@ public function testFileSubstitutions() { 'filename' => 'druplicon.txt', 'uri' => 'public://druplicon.txt', 'filemime' => 'text/plain', - 'status' => FILE_STATUS_PERMANENT, + 'status' => FileInterface::STATUS_PERMANENT, ]); $file->save(); - $this->assertEquals($GLOBALS['base_url'] . '/' . $this->siteDirectory . '/files/druplicon.txt', $fileSubstitution->getUrl($file)->getGeneratedUrl()); + $this->assertEquals('/' . $this->siteDirectory . '/files/druplicon.txt', $fileSubstitution->getUrl($file)->getGeneratedUrl()); $entity_type = $this->entityTypeManager->getDefinition('file'); $this->assertTrue(FileSubstitutionPlugin::isApplicable($entity_type), 'The entity type File is applicable the file substitution.'); @@ -158,7 +159,7 @@ public function testMediaSubstitution() { 'filename' => 'druplicon.txt', 'uri' => 'public://druplicon.txt', 'filemime' => 'text/plain', - 'status' => FILE_STATUS_PERMANENT, + 'status' => FileInterface::STATUS_PERMANENT, ]); $file->save(); @@ -169,7 +170,7 @@ public function testMediaSubstitution() { $media->save(); $media_substitution = $this->substitutionManager->createInstance('media'); - $expected = $GLOBALS['base_url'] . '/' . $this->siteDirectory . '/files/druplicon.txt'; + $expected = '/' . $this->siteDirectory . '/files/druplicon.txt'; $this->assertEquals($expected, $media_substitution->getUrl($media)->getGeneratedUrl()); // Ensure the url is identical when media entities have a standalone URL diff --git a/web/modules/linkit/tests/src/Kernel/ValidatorsTest.php b/web/modules/linkit/tests/src/Kernel/ValidatorsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9d7fa6122306ea259ca0bea18a056daf6e1ed86b --- /dev/null +++ b/web/modules/linkit/tests/src/Kernel/ValidatorsTest.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\linkit\Kernel; + +use Drupal\Tests\ckeditor5\Kernel\ValidatorsTest as CKEditor5CoreValidatorsTest; + +/** + * @covers \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit::validChoices + * @covers \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit::requireProfileIfEnabled + * @covers linkit.schema.yml + * + * @group linkit + */ +class ValidatorsTest extends CKEditor5CoreValidatorsTest { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'linkit', + // @see config/optional/linkit.linkit_profile.default.yml + 'node', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + // @see config/optional/linkit.linkit_profile.default.yml + $this->installConfig(['linkit']); + } + + /** + * {@inheritdoc} + */ + public function provider(): array { + $linkit_test_cases_toolbar_settings = ['items' => ['link']]; + + $data = []; + $data['VALID: installing the linkit module without configuring the existing text editors'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [], + ], + 'violations' => [], + ]; + $data['INVALID: linkit — invalid manually created configuration'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => 'no', + ], + ], + ], + 'violations' => [ + 'settings.plugins.linkit_extension.linkit_enabled' => 'This value should be of the correct primitive type.', + ], + ]; + $data['VALID: linkit off'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => FALSE, + ], + ], + ], + 'violations' => [], + ]; + $data['VALID: linkit off, profile selected'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'default', + ], + ], + ], + 'violations' => [], + ]; + $data['INVALID: linkit on, no profile selected'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + ], + ], + ], + 'violations' => [ + 'settings.plugins.linkit_extension.linkit_profile' => 'Linkit is enabled, please select the Linkit profile you wish to use.', + ], + ]; + $data['INVALID: linkit on, non-existent profile selected'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'nonexistent', + ], + ], + ], + 'violations' => [ + 'settings.plugins.linkit_extension.linkit_profile' => 'The value you selected is not a valid choice.', + ], + ]; + $data['VALID: linkit on, existing profile selected'] = [ + 'settings' => [ + 'toolbar' => $linkit_test_cases_toolbar_settings, + 'plugins' => [ + 'linkit_extension' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => 'default', + ], + ], + ], + 'violations' => [], + ]; + return $data; + } + + /** + * {@inheritdoc} + */ + public function providerPair(): array { + // Linkit is 100% independent of the text format, so no need for this test. + return []; + } + +} diff --git a/web/modules/linkit/webpack.config.js b/web/modules/linkit/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..afb1bbc82774b50c150bd3d3a5a85cabe7caffb4 --- /dev/null +++ b/web/modules/linkit/webpack.config.js @@ -0,0 +1,57 @@ +const path = require('path'); +const fs = require('fs'); +const webpack = require('webpack'); +const TerserPlugin = require('terser-webpack-plugin'); + +function getDirectories(srcpath) { + return fs + .readdirSync(srcpath) + .filter((item) => fs.statSync(path.join(srcpath, item)).isDirectory()); +} + +module.exports = []; +// Loop through every subdirectory in src, each a different plugin, and build +// each one in ./build. +getDirectories('./js/ckeditor5_plugins').forEach((dir) => { + const bc = { + mode: 'production', + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + terserOptions: { + format: { + comments: false, + }, + }, + test: /\.js(\?.*)?$/i, + extractComments: false, + }), + ], + moduleIds: 'named', + }, + entry: { + path: path.resolve(__dirname, 'js/ckeditor5_plugins', dir, 'src/index.js') + }, + output: { + path: path.resolve(__dirname, './js/build'), + filename: `${dir}.js`, + library: ['CKEditor5', dir], + libraryTarget: 'umd', + libraryExport: 'default' + }, + plugins: [ + new webpack.DllReferencePlugin({ + manifest: require('./node_modules/ckeditor5/build/ckeditor5-dll.manifest.json'), // eslint-disable-line global-require, import/no-unresolved + scope: 'ckeditor5/src', + name: 'CKEditor5.dll', + }), + ], + module: { + rules: [{ test: /\.svg$/, use: 'raw-loader' }], + }, + devtool: false, + }; + + module.exports.push(bc); +});