diff --git a/composer.lock b/composer.lock
index 8c6000c89a..a93571665a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -500,7 +500,7 @@
"dist": {
"type": "path",
"url": "core",
- "reference": "493f34276d92722b600d8cf70f7852260a87afd5"
+ "reference": "4e13d03bc47024576f020c51bec88003371858d9"
},
"require": {
"asm89/stack-cors": "^1.1",
diff --git a/core/.eslintrc.json b/core/.eslintrc.json
index 5a982db5a2..6d5c3d73a2 100644
--- a/core/.eslintrc.json
+++ b/core/.eslintrc.json
@@ -20,7 +20,8 @@
"Modernizr": true,
"Popper": true,
"Sortable": true,
- "CKEDITOR": true
+ "CKEDITOR": true,
+ "DrupalAutocomplete": true
},
"settings": {
"react": {
diff --git a/core/assets/vendor/fetch/fetch.umd.js b/core/assets/vendor/fetch/fetch.umd.js
new file mode 100644
index 0000000000..1b2131e29a
--- /dev/null
+++ b/core/assets/vendor/fetch/fetch.umd.js
@@ -0,0 +1,620 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (factory((global.WHATWGFetch = {})));
+}(this, (function (exports) { 'use strict';
+
+ var global =
+ (typeof globalThis !== 'undefined' && globalThis) ||
+ (typeof self !== 'undefined' && self) ||
+ (typeof global !== 'undefined' && global);
+
+ var support = {
+ searchParams: 'URLSearchParams' in global,
+ iterable: 'Symbol' in global && 'iterator' in Symbol,
+ blob:
+ 'FileReader' in global &&
+ 'Blob' in global &&
+ (function() {
+ try {
+ new Blob();
+ return true
+ } catch (e) {
+ return false
+ }
+ })(),
+ formData: 'FormData' in global,
+ arrayBuffer: 'ArrayBuffer' in global
+ };
+
+ function isDataView(obj) {
+ return obj && DataView.prototype.isPrototypeOf(obj)
+ }
+
+ if (support.arrayBuffer) {
+ var viewClasses = [
+ '[object Int8Array]',
+ '[object Uint8Array]',
+ '[object Uint8ClampedArray]',
+ '[object Int16Array]',
+ '[object Uint16Array]',
+ '[object Int32Array]',
+ '[object Uint32Array]',
+ '[object Float32Array]',
+ '[object Float64Array]'
+ ];
+
+ var isArrayBufferView =
+ ArrayBuffer.isView ||
+ function(obj) {
+ return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
+ };
+ }
+
+ function normalizeName(name) {
+ if (typeof name !== 'string') {
+ name = String(name);
+ }
+ if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
+ throw new TypeError('Invalid character in header field name')
+ }
+ return name.toLowerCase()
+ }
+
+ function normalizeValue(value) {
+ if (typeof value !== 'string') {
+ value = String(value);
+ }
+ return value
+ }
+
+ // Build a destructive iterator for the value list
+ function iteratorFor(items) {
+ var iterator = {
+ next: function() {
+ var value = items.shift();
+ return {done: value === undefined, value: value}
+ }
+ };
+
+ if (support.iterable) {
+ iterator[Symbol.iterator] = function() {
+ return iterator
+ };
+ }
+
+ return iterator
+ }
+
+ function Headers(headers) {
+ this.map = {};
+
+ if (headers instanceof Headers) {
+ headers.forEach(function(value, name) {
+ this.append(name, value);
+ }, this);
+ } else if (Array.isArray(headers)) {
+ headers.forEach(function(header) {
+ this.append(header[0], header[1]);
+ }, this);
+ } else if (headers) {
+ Object.getOwnPropertyNames(headers).forEach(function(name) {
+ this.append(name, headers[name]);
+ }, this);
+ }
+ }
+
+ Headers.prototype.append = function(name, value) {
+ name = normalizeName(name);
+ value = normalizeValue(value);
+ var oldValue = this.map[name];
+ this.map[name] = oldValue ? oldValue + ', ' + value : value;
+ };
+
+ Headers.prototype['delete'] = function(name) {
+ delete this.map[normalizeName(name)];
+ };
+
+ Headers.prototype.get = function(name) {
+ name = normalizeName(name);
+ return this.has(name) ? this.map[name] : null
+ };
+
+ Headers.prototype.has = function(name) {
+ return this.map.hasOwnProperty(normalizeName(name))
+ };
+
+ Headers.prototype.set = function(name, value) {
+ this.map[normalizeName(name)] = normalizeValue(value);
+ };
+
+ Headers.prototype.forEach = function(callback, thisArg) {
+ for (var name in this.map) {
+ if (this.map.hasOwnProperty(name)) {
+ callback.call(thisArg, this.map[name], name, this);
+ }
+ }
+ };
+
+ Headers.prototype.keys = function() {
+ var items = [];
+ this.forEach(function(value, name) {
+ items.push(name);
+ });
+ return iteratorFor(items)
+ };
+
+ Headers.prototype.values = function() {
+ var items = [];
+ this.forEach(function(value) {
+ items.push(value);
+ });
+ return iteratorFor(items)
+ };
+
+ Headers.prototype.entries = function() {
+ var items = [];
+ this.forEach(function(value, name) {
+ items.push([name, value]);
+ });
+ return iteratorFor(items)
+ };
+
+ if (support.iterable) {
+ Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
+ }
+
+ function consumed(body) {
+ if (body.bodyUsed) {
+ return Promise.reject(new TypeError('Already read'))
+ }
+ body.bodyUsed = true;
+ }
+
+ function fileReaderReady(reader) {
+ return new Promise(function(resolve, reject) {
+ reader.onload = function() {
+ resolve(reader.result);
+ };
+ reader.onerror = function() {
+ reject(reader.error);
+ };
+ })
+ }
+
+ function readBlobAsArrayBuffer(blob) {
+ var reader = new FileReader();
+ var promise = fileReaderReady(reader);
+ reader.readAsArrayBuffer(blob);
+ return promise
+ }
+
+ function readBlobAsText(blob) {
+ var reader = new FileReader();
+ var promise = fileReaderReady(reader);
+ reader.readAsText(blob);
+ return promise
+ }
+
+ function readArrayBufferAsText(buf) {
+ var view = new Uint8Array(buf);
+ var chars = new Array(view.length);
+
+ for (var i = 0; i < view.length; i++) {
+ chars[i] = String.fromCharCode(view[i]);
+ }
+ return chars.join('')
+ }
+
+ function bufferClone(buf) {
+ if (buf.slice) {
+ return buf.slice(0)
+ } else {
+ var view = new Uint8Array(buf.byteLength);
+ view.set(new Uint8Array(buf));
+ return view.buffer
+ }
+ }
+
+ function Body() {
+ this.bodyUsed = false;
+
+ this._initBody = function(body) {
+ /*
+ fetch-mock wraps the Response object in an ES6 Proxy to
+ provide useful test harness features such as flush. However, on
+ ES5 browsers without fetch or Proxy support pollyfills must be used;
+ the proxy-pollyfill is unable to proxy an attribute unless it exists
+ on the object before the Proxy is created. This change ensures
+ Response.bodyUsed exists on the instance, while maintaining the
+ semantic of setting Request.bodyUsed in the constructor before
+ _initBody is called.
+ */
+ this.bodyUsed = this.bodyUsed;
+ this._bodyInit = body;
+ if (!body) {
+ this._bodyText = '';
+ } else if (typeof body === 'string') {
+ this._bodyText = body;
+ } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
+ this._bodyBlob = body;
+ } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
+ this._bodyFormData = body;
+ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
+ this._bodyText = body.toString();
+ } else if (support.arrayBuffer && support.blob && isDataView(body)) {
+ this._bodyArrayBuffer = bufferClone(body.buffer);
+ // IE 10-11 can't handle a DataView body.
+ this._bodyInit = new Blob([this._bodyArrayBuffer]);
+ } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
+ this._bodyArrayBuffer = bufferClone(body);
+ } else {
+ this._bodyText = body = Object.prototype.toString.call(body);
+ }
+
+ if (!this.headers.get('content-type')) {
+ if (typeof body === 'string') {
+ this.headers.set('content-type', 'text/plain;charset=UTF-8');
+ } else if (this._bodyBlob && this._bodyBlob.type) {
+ this.headers.set('content-type', this._bodyBlob.type);
+ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
+ this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
+ }
+ }
+ };
+
+ if (support.blob) {
+ this.blob = function() {
+ var rejected = consumed(this);
+ if (rejected) {
+ return rejected
+ }
+
+ if (this._bodyBlob) {
+ return Promise.resolve(this._bodyBlob)
+ } else if (this._bodyArrayBuffer) {
+ return Promise.resolve(new Blob([this._bodyArrayBuffer]))
+ } else if (this._bodyFormData) {
+ throw new Error('could not read FormData body as blob')
+ } else {
+ return Promise.resolve(new Blob([this._bodyText]))
+ }
+ };
+
+ this.arrayBuffer = function() {
+ if (this._bodyArrayBuffer) {
+ var isConsumed = consumed(this);
+ if (isConsumed) {
+ return isConsumed
+ }
+ if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
+ return Promise.resolve(
+ this._bodyArrayBuffer.buffer.slice(
+ this._bodyArrayBuffer.byteOffset,
+ this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
+ )
+ )
+ } else {
+ return Promise.resolve(this._bodyArrayBuffer)
+ }
+ } else {
+ return this.blob().then(readBlobAsArrayBuffer)
+ }
+ };
+ }
+
+ this.text = function() {
+ var rejected = consumed(this);
+ if (rejected) {
+ return rejected
+ }
+
+ if (this._bodyBlob) {
+ return readBlobAsText(this._bodyBlob)
+ } else if (this._bodyArrayBuffer) {
+ return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
+ } else if (this._bodyFormData) {
+ throw new Error('could not read FormData body as text')
+ } else {
+ return Promise.resolve(this._bodyText)
+ }
+ };
+
+ if (support.formData) {
+ this.formData = function() {
+ return this.text().then(decode)
+ };
+ }
+
+ this.json = function() {
+ return this.text().then(JSON.parse)
+ };
+
+ return this
+ }
+
+ // HTTP methods whose capitalization should be normalized
+ var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'];
+
+ function normalizeMethod(method) {
+ var upcased = method.toUpperCase();
+ return methods.indexOf(upcased) > -1 ? upcased : method
+ }
+
+ function Request(input, options) {
+ if (!(this instanceof Request)) {
+ throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
+ }
+
+ options = options || {};
+ var body = options.body;
+
+ if (input instanceof Request) {
+ if (input.bodyUsed) {
+ throw new TypeError('Already read')
+ }
+ this.url = input.url;
+ this.credentials = input.credentials;
+ if (!options.headers) {
+ this.headers = new Headers(input.headers);
+ }
+ this.method = input.method;
+ this.mode = input.mode;
+ this.signal = input.signal;
+ if (!body && input._bodyInit != null) {
+ body = input._bodyInit;
+ input.bodyUsed = true;
+ }
+ } else {
+ this.url = String(input);
+ }
+
+ this.credentials = options.credentials || this.credentials || 'same-origin';
+ if (options.headers || !this.headers) {
+ this.headers = new Headers(options.headers);
+ }
+ this.method = normalizeMethod(options.method || this.method || 'GET');
+ this.mode = options.mode || this.mode || null;
+ this.signal = options.signal || this.signal;
+ this.referrer = null;
+
+ if ((this.method === 'GET' || this.method === 'HEAD') && body) {
+ throw new TypeError('Body not allowed for GET or HEAD requests')
+ }
+ this._initBody(body);
+
+ if (this.method === 'GET' || this.method === 'HEAD') {
+ if (options.cache === 'no-store' || options.cache === 'no-cache') {
+ // Search for a '_' parameter in the query string
+ var reParamSearch = /([?&])_=[^&]*/;
+ if (reParamSearch.test(this.url)) {
+ // If it already exists then set the value with the current time
+ this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
+ } else {
+ // Otherwise add a new '_' parameter to the end with the current time
+ var reQueryString = /\?/;
+ this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
+ }
+ }
+ }
+ }
+
+ Request.prototype.clone = function() {
+ return new Request(this, {body: this._bodyInit})
+ };
+
+ function decode(body) {
+ var form = new FormData();
+ body
+ .trim()
+ .split('&')
+ .forEach(function(bytes) {
+ if (bytes) {
+ var split = bytes.split('=');
+ var name = split.shift().replace(/\+/g, ' ');
+ var value = split.join('=').replace(/\+/g, ' ');
+ form.append(decodeURIComponent(name), decodeURIComponent(value));
+ }
+ });
+ return form
+ }
+
+ function parseHeaders(rawHeaders) {
+ var headers = new Headers();
+ // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
+ // https://tools.ietf.org/html/rfc7230#section-3.2
+ var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
+ // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
+ // https://github.com/github/fetch/issues/748
+ // https://github.com/zloirock/core-js/issues/751
+ preProcessedHeaders
+ .split('\r')
+ .map(function(header) {
+ return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
+ })
+ .forEach(function(line) {
+ var parts = line.split(':');
+ var key = parts.shift().trim();
+ if (key) {
+ var value = parts.join(':').trim();
+ headers.append(key, value);
+ }
+ });
+ return headers
+ }
+
+ Body.call(Request.prototype);
+
+ function Response(bodyInit, options) {
+ if (!(this instanceof Response)) {
+ throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
+ }
+ if (!options) {
+ options = {};
+ }
+
+ this.type = 'default';
+ this.status = options.status === undefined ? 200 : options.status;
+ this.ok = this.status >= 200 && this.status < 300;
+ this.statusText = 'statusText' in options ? options.statusText : '';
+ this.headers = new Headers(options.headers);
+ this.url = options.url || '';
+ this._initBody(bodyInit);
+ }
+
+ Body.call(Response.prototype);
+
+ Response.prototype.clone = function() {
+ return new Response(this._bodyInit, {
+ status: this.status,
+ statusText: this.statusText,
+ headers: new Headers(this.headers),
+ url: this.url
+ })
+ };
+
+ Response.error = function() {
+ var response = new Response(null, {status: 0, statusText: ''});
+ response.type = 'error';
+ return response
+ };
+
+ var redirectStatuses = [301, 302, 303, 307, 308];
+
+ Response.redirect = function(url, status) {
+ if (redirectStatuses.indexOf(status) === -1) {
+ throw new RangeError('Invalid status code')
+ }
+
+ return new Response(null, {status: status, headers: {location: url}})
+ };
+
+ exports.DOMException = global.DOMException;
+ try {
+ new exports.DOMException();
+ } catch (err) {
+ exports.DOMException = function(message, name) {
+ this.message = message;
+ this.name = name;
+ var error = Error(message);
+ this.stack = error.stack;
+ };
+ exports.DOMException.prototype = Object.create(Error.prototype);
+ exports.DOMException.prototype.constructor = exports.DOMException;
+ }
+
+ function fetch(input, init) {
+ return new Promise(function(resolve, reject) {
+ var request = new Request(input, init);
+
+ if (request.signal && request.signal.aborted) {
+ return reject(new exports.DOMException('Aborted', 'AbortError'))
+ }
+
+ var xhr = new XMLHttpRequest();
+
+ function abortXhr() {
+ xhr.abort();
+ }
+
+ xhr.onload = function() {
+ var options = {
+ status: xhr.status,
+ statusText: xhr.statusText,
+ headers: parseHeaders(xhr.getAllResponseHeaders() || '')
+ };
+ options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
+ var body = 'response' in xhr ? xhr.response : xhr.responseText;
+ setTimeout(function() {
+ resolve(new Response(body, options));
+ }, 0);
+ };
+
+ xhr.onerror = function() {
+ setTimeout(function() {
+ reject(new TypeError('Network request failed'));
+ }, 0);
+ };
+
+ xhr.ontimeout = function() {
+ setTimeout(function() {
+ reject(new TypeError('Network request failed'));
+ }, 0);
+ };
+
+ xhr.onabort = function() {
+ setTimeout(function() {
+ reject(new exports.DOMException('Aborted', 'AbortError'));
+ }, 0);
+ };
+
+ function fixUrl(url) {
+ try {
+ return url === '' && global.location.href ? global.location.href : url
+ } catch (e) {
+ return url
+ }
+ }
+
+ xhr.open(request.method, fixUrl(request.url), true);
+
+ if (request.credentials === 'include') {
+ xhr.withCredentials = true;
+ } else if (request.credentials === 'omit') {
+ xhr.withCredentials = false;
+ }
+
+ if ('responseType' in xhr) {
+ if (support.blob) {
+ xhr.responseType = 'blob';
+ } else if (
+ support.arrayBuffer &&
+ request.headers.get('Content-Type') &&
+ request.headers.get('Content-Type').indexOf('application/octet-stream') !== -1
+ ) {
+ xhr.responseType = 'arraybuffer';
+ }
+ }
+
+ if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers)) {
+ Object.getOwnPropertyNames(init.headers).forEach(function(name) {
+ xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
+ });
+ } else {
+ request.headers.forEach(function(value, name) {
+ xhr.setRequestHeader(name, value);
+ });
+ }
+
+ if (request.signal) {
+ request.signal.addEventListener('abort', abortXhr);
+
+ xhr.onreadystatechange = function() {
+ // DONE (success or failure)
+ if (xhr.readyState === 4) {
+ request.signal.removeEventListener('abort', abortXhr);
+ }
+ };
+ }
+
+ xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
+ })
+ }
+
+ fetch.polyfill = true;
+
+ if (!global.fetch) {
+ global.fetch = fetch;
+ global.Headers = Headers;
+ global.Request = Request;
+ global.Response = Response;
+ }
+
+ exports.Headers = Headers;
+ exports.Request = Request;
+ exports.Response = Response;
+ exports.fetch = fetch;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index bcc0db0553..e363e317b5 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -132,6 +132,11 @@ drupal.collapse:
- core/drupal.form
- core/jquery.once
+drupal.customevent:
+ version: VERSION
+ js:
+ misc/polyfills/customevent.js: { weight: -20 }
+
drupal.date:
version: VERSION
js:
@@ -196,6 +201,11 @@ drupal.dropbutton:
- core/drupalSettings
- core/jquery.once
+drupal.element.closest:
+ version: VERSION
+ js:
+ misc/polyfills/element.closest.js: { weight: -20 }
+
drupal.entity-form:
version: VERSION
js:
@@ -203,6 +213,17 @@ drupal.entity-form:
dependencies:
- core/drupal.form
+drupal.fetch:
+ version: "3.4.0"
+ license:
+ name: MIT
+ url: https://raw.githubusercontent.com/github/fetch/v3.4.1/LICENSE
+ gpl-compatible: true
+ js:
+ assets/vendor/fetch/fetch.umd.js: {}
+ dependencies:
+ - core/es6-promise
+
drupal.form:
version: VERSION
js:
@@ -465,7 +486,7 @@ jquery.ui.autocomplete:
- core/jquery.ui.widget
- core/jquery.ui.position
- core/jquery.ui.menu
- deprecated: The "%library_id%" asset library is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. See https://www.drupal.org/project/drupal/issues/3076171
+ deprecated: The "%library_id%" asset library is deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. https://www.drupal.org/node/3083715
jquery.ui.button:
version: *jquery_ui_version
diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
index 3ccc1fbcc4..b5b15e01fa 100644
--- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
+++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
@@ -3,6 +3,7 @@
namespace Drupal\Core\Entity\Element;
use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
@@ -188,20 +189,26 @@ public static function processEntityAutocomplete(array &$element, FormStateInter
'selection_settings_key' => $selection_settings_key,
];
+
// Create attribute that will be used for client-side enforcement of field
// cardinality.
- $name = explode('[', $element['#name'])[0];
- if (!empty($complete_form[$name]['widget']['#theme']) && $complete_form[$name]['widget']['#theme'] === 'field_multiple_value_form') {
+ $element_parents_in_form = array_slice($element['#parents'], 0, count($element['#parents']) -2);
+ $element_within_form = NestedArray::getValue($complete_form, $element_parents_in_form, $key_exists);
+
+ // If the input is a inside a multiple value widget, limit cardinality to 1
+ // as there is a dedicated input for each allowed value.
+ if (!empty($element_within_form['widget']['#theme']) && $element_within_form['widget']['#theme'] === 'field_multiple_value_form') {
$cardinality = 1;
}
- elseif (!empty($complete_form[$name]['widget']['#cardinality'])) {
- $cardinality = $complete_form[$name]['widget']['#cardinality'];
+ elseif (!empty($element_within_form['widget']['#cardinality'])) {
+ $cardinality = $element_within_form['widget']['#cardinality'];
}
else {
+ // A Cardinality of -1 is considered unlimited.
$cardinality = -1;
}
- $complete_form[$name]['#attributes']['data-autocomplete-cardinality'] = $cardinality;
+ $element['#attributes']['data-autocomplete-cardinality'] = $cardinality;
return $element;
}
diff --git a/core/misc/polyfills/customevent.es6.js b/core/misc/polyfills/customevent.es6.js
new file mode 100644
index 0000000000..be85a56cb8
--- /dev/null
+++ b/core/misc/polyfills/customevent.es6.js
@@ -0,0 +1,30 @@
+/**
+ * @file
+ * Provides a polyfill for CustomEvent.
+ *
+ * This is needed for Internet Explorer 11.
+ *
+ * This has been copied from MDN Web Docs code samples. Code samples in the MDN
+ * Web Docs are licensed under CC0.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
+ * @see https://developer.mozilla.org/en-US/docs/MDN/About#Code_samples_and_snippets
+ */
+// eslint-disable-next-line func-names
+(function () {
+ if (typeof window.CustomEvent === 'function') return false;
+
+ function CustomEvent(event, params) {
+ params = params || { bubbles: false, cancelable: false, detail: null };
+ const evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(
+ event,
+ params.bubbles,
+ params.cancelable,
+ params.detail,
+ );
+ return evt;
+ }
+
+ window.CustomEvent = CustomEvent;
+})();
diff --git a/core/misc/polyfills/customevent.js b/core/misc/polyfills/customevent.js
new file mode 100644
index 0000000000..87e371ed1c
--- /dev/null
+++ b/core/misc/polyfills/customevent.js
@@ -0,0 +1,23 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function () {
+ if (typeof window.CustomEvent === 'function') return false;
+
+ function CustomEvent(event, params) {
+ params = params || {
+ bubbles: false,
+ cancelable: false,
+ detail: null
+ };
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt;
+ }
+
+ window.CustomEvent = CustomEvent;
+})();
\ No newline at end of file
diff --git a/core/misc/polyfills/element.closest.es6.js b/core/misc/polyfills/element.closest.es6.js
new file mode 100644
index 0000000000..2833c5759d
--- /dev/null
+++ b/core/misc/polyfills/element.closest.es6.js
@@ -0,0 +1,30 @@
+/**
+ * @file
+ * Provides a polyfill for Element.closest().
+ *
+ * This is needed for Internet Explorer 11 and Opera Mini.
+ *
+ * This has been copied from MDN Web Docs code samples. Code samples in the MDN
+ * Web Docs are licensed under CC0.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
+ * @see https://developer.mozilla.org/en-US/docs/MDN/About#Code_samples_and_snippets
+ */
+if (!Element.prototype.matches) {
+ Element.prototype.matches =
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.webkitMatchesSelector;
+}
+
+if (!Element.prototype.closest) {
+ // eslint-disable-next-line func-names
+ Element.prototype.closest = function (s) {
+ let el = this;
+
+ do {
+ if (Element.prototype.matches.call(el, s)) return el;
+ el = el.parentElement || el.parentNode;
+ } while (el !== null && el.nodeType === 1);
+ return null;
+ };
+}
diff --git a/core/misc/polyfills/element.closest.js b/core/misc/polyfills/element.closest.js
new file mode 100644
index 0000000000..c1317ce883
--- /dev/null
+++ b/core/misc/polyfills/element.closest.js
@@ -0,0 +1,23 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+if (!Element.prototype.matches) {
+ Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
+}
+
+if (!Element.prototype.closest) {
+ Element.prototype.closest = function (s) {
+ var el = this;
+
+ do {
+ if (Element.prototype.matches.call(el, s)) return el;
+ el = el.parentElement || el.parentNode;
+ } while (el !== null && el.nodeType === 1);
+
+ return null;
+ };
+}
\ No newline at end of file
diff --git a/core/modules/d9_autocomplete/css/d9_autocomplete.css b/core/modules/d9_autocomplete/css/d9_autocomplete.css
index 24441afe8d..d8e022044b 100644
--- a/core/modules/d9_autocomplete/css/d9_autocomplete.css
+++ b/core/modules/d9_autocomplete/css/d9_autocomplete.css
@@ -1,23 +1,29 @@
+[data-drupal-autocomplete-wrapper] {
+ position: relative;
+}
+
[data-drupal-autocomplete-list] {
- z-index: 1;
+ position: absolute;
margin: 0;
padding: 0;
list-style: none;
}
-
[data-drupal-autocomplete-list][hidden],
[data-drupal-autocomplete-list]:empty {
display: none;
}
-
[data-drupal-autocomplete-list] li {
- padding: 3px 1em 3px 0.4em;
+ display: list-item;
}
-
-[data-drupal-autocomplete-list] li a {
- display: block;
-}
-
[data-drupal-autocomplete-list] li a:hover {
cursor: pointer;
}
+
+[data-drupal-autocomplete-live-region] {
+ position: absolute !important;
+ overflow: hidden;
+ clip: rect(1px, 1px, 1px, 1px);
+ width: 1px;
+ height: 1px;
+ word-wrap: normal;
+}
diff --git a/core/modules/d9_autocomplete/css/jqueryui.css b/core/modules/d9_autocomplete/css/jqueryui.css
new file mode 100644
index 0000000000..73f18993ef
--- /dev/null
+++ b/core/modules/d9_autocomplete/css/jqueryui.css
@@ -0,0 +1,15 @@
+/* Styling specific to d9_autocomplete */
+[data-drupal-autocomplete-wrapper] .ui-menu-item-wrapper {
+ position: relative;
+ display: block;
+ padding: 3px 1em 3px 0.4em;
+}
+
+[data-drupal-autocomplete-wrapper] .ui-menu-item-wrapper:hover {
+ color: #840;
+}
+
+.ui-autocomplete {
+ border: 1px solid #ccc;
+ background: #fff;
+}
diff --git a/core/modules/d9_autocomplete/d9_autocomplete.info.yml b/core/modules/d9_autocomplete/d9_autocomplete.info.yml
index 26a8c4c5a7..73b1a6ea3e 100644
--- a/core/modules/d9_autocomplete/d9_autocomplete.info.yml
+++ b/core/modules/d9_autocomplete/d9_autocomplete.info.yml
@@ -1,4 +1,4 @@
name: 'Drupal 9 Autocomplete'
type: module
-description: 'Provides a replacement for jQuery UI Autocomplete. jQuery UI Autocomplete is deprecated in Drupal 8.8.0 and will be removed in Drupal 9.0.'
+description: 'Provides a replacement for jQuery UI Autocomplete. jQuery UI Autocomplete is deprecated in Drupal 9.2.0 and will be removed in Drupal 10.0.'
package: 'Core (Experimental)'
diff --git a/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml b/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml
index 1295942fd5..6e71b600a2 100644
--- a/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml
+++ b/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml
@@ -1,16 +1,35 @@
autocomplete:
version: VERSION
js:
- js/autocomplete.js: {}
+ js/drupalautocomplete.js: {}
+ js/autocomplete-init.js: {}
css:
component:
+ css/jqueryui.css: { weight: -1 }
css/d9_autocomplete.css: {}
+ drupalSettings:
+ autocompleteOptions:
+ inputClass: ui-autocomplete-input
+ ulClass: ui-menu ui-widget ui-widget-content ui-autocomplete ui-front
+ loadingClass: ui-autocomplete-loading
+ itemClass: ui-menu-item
+ liveRegion: false
dependencies:
- core/drupal
- core/drupalSettings
- core/drupal.ajax
- core/drupal.announce
+ - core/drupal.customevent
+ - core/drupal.element.closest
+ - core/drupal.fetch
- core/popperjs
- # Needed for once() and event listeners.
+ - core/jqueryui.autocomplete.styles
+ # jQuery needed for once() and event management.
- core/jquery
- core/jquery.once
+
+jqueryui.autocomplete.styles:
+ version: VERSION
+ css:
+ component:
+ assets/vendor/jquery.ui/themes/base/autocomplete.css: {}
diff --git a/core/modules/d9_autocomplete/js/autocomplete-init.es6.js b/core/modules/d9_autocomplete/js/autocomplete-init.es6.js
new file mode 100644
index 0000000000..ec72364aa1
--- /dev/null
+++ b/core/modules/d9_autocomplete/js/autocomplete-init.es6.js
@@ -0,0 +1,69 @@
+(($, Drupal, drupalSettings, DrupalAutocomplete) => {
+ Drupal.Autocomplete = {};
+ Drupal.Autocomplete.instances = [];
+
+ /**
+ * Attaches the autocomplete behavior to all required fields.
+ *
+ * @type {Drupal~behavior}
+ *
+ * @prop {Drupal~behaviorAttach} attach
+ * Attaches the autocomplete behaviors.
+ */
+ Drupal.behaviors.autocomplete = {
+ attach(context) {
+ const options = drupalSettings.autocompleteOptions || {};
+ const $autoCompleteInputs = $(context)
+ .find('input.form-autocomplete')
+ .once('autocomplete-init');
+
+ DrupalAutocomplete.formatSuggestionItem = (suggestion) =>
+ // Wrap the item text in an ``, This tag is not added by default
+ // as it's not needed for functionality. However, Claro and Seven
+ // both have styles assuming the presence of this tag.
+ ``;
+
+ function autocompleteResultsMessage(count) {
+ const { maxItems } = this.options;
+ if (count === 0) {
+ return Drupal.t('No results found');
+ }
+
+ const pluralMessage =
+ maxItems === count
+ ? 'There are at least @count results available. Type additional characters to refine your search.'
+ : 'There are @count results available.';
+ return Drupal.formatPlural(
+ count,
+ 'There is one result available.',
+ pluralMessage,
+ );
+ }
+
+ function autocompleteAnnounceResults(count) {
+ const message = this.resultsMessage(count);
+ Drupal.announce(message, 'assertive');
+ }
+
+ $autoCompleteInputs.each((index, autocompleteInput) => {
+ const id = autocompleteInput.getAttribute('id');
+ Drupal.Autocomplete.instances[id] = new DrupalAutocomplete(
+ autocompleteInput,
+ options,
+ );
+ Drupal.Autocomplete.instances[
+ id
+ ].resultsMessage = autocompleteResultsMessage;
+ Drupal.Autocomplete.instances[
+ id
+ ].announceResults = autocompleteAnnounceResults;
+ });
+ },
+ detach(context) {
+ context.querySelectorAll('input.form-autocomplete').forEach((input) => {
+ const id = input.getAttribute('id');
+ Drupal.Autocomplete.instances[id].destroy();
+ });
+ },
+ };
+})(jQuery, Drupal, drupalSettings, DrupalAutocomplete);
diff --git a/core/modules/d9_autocomplete/js/autocomplete-init.js b/core/modules/d9_autocomplete/js/autocomplete-init.js
new file mode 100644
index 0000000000..68d747ba5d
--- /dev/null
+++ b/core/modules/d9_autocomplete/js/autocomplete-init.js
@@ -0,0 +1,50 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, Drupal, drupalSettings, DrupalAutocomplete) {
+ Drupal.Autocomplete = {};
+ Drupal.Autocomplete.instances = [];
+ Drupal.behaviors.autocomplete = {
+ attach: function attach(context) {
+ var options = drupalSettings.autocompleteOptions || {};
+ var $autoCompleteInputs = $(context).find('input.form-autocomplete').once('autocomplete-init');
+
+ DrupalAutocomplete.formatSuggestionItem = function (suggestion) {
+ return "");
+ };
+
+ function autocompleteResultsMessage(count) {
+ var maxItems = this.options.maxItems;
+
+ if (count === 0) {
+ return Drupal.t('No results found');
+ }
+
+ var pluralMessage = maxItems === count ? 'There are at least @count results available. Type additional characters to refine your search.' : 'There are @count results available.';
+ return Drupal.formatPlural(count, 'There is one result available.', pluralMessage);
+ }
+
+ function autocompleteAnnounceResults(count) {
+ var message = this.resultsMessage(count);
+ Drupal.announce(message, 'assertive');
+ }
+
+ $autoCompleteInputs.each(function (index, autocompleteInput) {
+ var id = autocompleteInput.getAttribute('id');
+ Drupal.Autocomplete.instances[id] = new DrupalAutocomplete(autocompleteInput, options);
+ Drupal.Autocomplete.instances[id].resultsMessage = autocompleteResultsMessage;
+ Drupal.Autocomplete.instances[id].announceResults = autocompleteAnnounceResults;
+ });
+ },
+ detach: function detach(context) {
+ context.querySelectorAll('input.form-autocomplete').forEach(function (input) {
+ var id = input.getAttribute('id');
+ Drupal.Autocomplete.instances[id].destroy();
+ });
+ }
+ };
+})(jQuery, Drupal, drupalSettings, DrupalAutocomplete);
\ No newline at end of file
diff --git a/core/modules/d9_autocomplete/js/autocomplete.es6.js b/core/modules/d9_autocomplete/js/autocomplete.es6.js
deleted file mode 100644
index c27d35e534..0000000000
--- a/core/modules/d9_autocomplete/js/autocomplete.es6.js
+++ /dev/null
@@ -1,492 +0,0 @@
-/**
- * @file
- * Standalone autocomplete.
- */
-
-(($, Drupal, drupalSettings) => {
- Drupal.Autocomplete = class {
- constructor(input, options = {}) {
- this.keyCode = Object.freeze({
- TAB: 9,
- RETURN: 13,
- ESC: 27,
- SPACE: 32,
- PAGEUP: 33,
- PAGEDOWN: 34,
- END: 35,
- HOME: 36,
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40,
- });
-
- this.count = document.querySelectorAll(
- '[data-drupal-autocomplete-initialized]',
- ).length;
- const listboxId = `autocomplete-listbox-${this.count}`;
-
- const defaultOptions = {
- firstCharacterDenylist: ',',
- minChars: 1,
- maxItems: 10,
- sort: false,
- };
- this.options = { ...defaultOptions, ...options };
- this.preventClose = false;
- this.isOpened = false;
- this.cache = [];
- this.suggestionItems = [];
- this.hasAnnouncedOnce = false;
- this.input = input;
- this.input.setAttribute('data-drupal-autocomplete-initialized', '');
- this.input.setAttribute('aria-autocomplete', 'list');
- this.input.setAttribute('autocomplete', 'off');
- this.input.setAttribute('data-drupal-autocomplete-input', '');
- this.input.setAttribute('aria-owns', listboxId);
- this.input.setAttribute('role', 'combobox');
- this.input.setAttribute('aria-expanded', 'false');
- this.ul = document.createElement('ul');
- this.ul.setAttribute('role', 'listbox');
- this.ul.setAttribute('data-drupal-autocomplete-list', '');
- this.ul.setAttribute('id', listboxId);
- this.ul.setAttribute('hidden', '');
- this.input.parentNode.appendChild(this.ul);
-
- // Add classes that were previously provided by jQuery UI.
- this.input.classList.add('ui-autocomplete-input');
- this.ul.classList.add('ui-autocomplete');
-
- $(this.input).on('input', () => this.inputListener());
- $(this.input).on('blur', (e) => this.blurHandler(e));
- $(this.input).on('keydown', (e) => this.inputKeyDown(e));
- $(this.ul).on('mousedown', (e) => e.preventDefault());
- $(this.ul).on('click', (e) => this.itemClick(e));
- $(this.ul).on('keydown', (e) => this.listKeyDown(e));
- $(this.ul).on('blur', (e) => this.blurHandler(e));
- }
-
- /**
- * Handles blur events.
- *
- * @param {Event} e
- * The blur event.
- */
- blurHandler(e) {
- if (this.preventClose) {
- this.preventClose = false;
- e.preventDefault();
- } else {
- this.close();
- }
- }
-
- listKeyDown(e) {
- if (
- !this.ul.contains(document.activeElement) ||
- e.ctrlKey ||
- e.altKey ||
- e.metaKey ||
- e.keyCode === this.keyCode.TAB
- ) {
- return;
- }
-
- switch (e.keyCode) {
- case this.keyCode.SPACE:
- case this.keyCode.RETURN:
- this.replaceInputValue(document.activeElement.textContent);
- this.close();
- this.input.focus();
- break;
-
- case this.keyCode.ESC:
- case this.keyCode.TAB:
- this.input.focus();
- this.close();
- break;
-
- case this.keyCode.UP:
- this.focusPrev();
- break;
-
- case this.keyCode.DOWN:
- this.focusNext();
- break;
-
- default:
- break;
- }
-
- e.stopPropagation();
- e.preventDefault();
- }
-
- focusPrev() {
- this.preventClose = true;
- const currentItem = document.activeElement.getAttribute(
- 'data-drupal-autocomplete-item',
- );
- const prevIndex = parseInt(currentItem, 10) - 1;
- const previousItem = this.ul.querySelector(
- `[data-drupal-autocomplete-item="${prevIndex}"]`,
- );
- if (previousItem) {
- previousItem.focus();
- }
- }
-
- focusNext() {
- this.preventClose = true;
- const currentItem = document.activeElement.getAttribute(
- 'data-drupal-autocomplete-item',
- );
- const nextIndex = parseInt(currentItem, 10) + 1;
- const nextItem = this.ul.querySelector(
- `[data-drupal-autocomplete-item="${nextIndex}"]`,
- );
- if (nextItem) {
- nextItem.focus();
- }
- }
-
- inputKeyDown(e) {
- const { keyCode } = e;
- if (this.isOpened) {
- if (keyCode === this.keyCode.ESC) {
- this.close();
- }
- if (keyCode === this.keyCode.DOWN) {
- e.preventDefault();
- this.preventClose = true;
- this.ul.querySelector('li').focus();
- }
- }
- }
-
- itemClick(e) {
- const li = e.target;
- if (li && e.button === 0) {
- this.replaceInputValue(li.textContent);
- e.preventDefault();
- this.close();
- }
- }
-
- /**
- * Replaces the value of an input field when a new value is chosen.
- *
- * @param {string} item
- * The item being added to the field.
- */
- replaceInputValue(item) {
- const cardinality = this.getCardinality();
- const numItems = this.splitValues().length;
-
- // Add a comma separator if the field allows additional items.
- const separator =
- numItems < cardinality || parseInt(cardinality, 10) === 0 ? ',' : '';
- const before = this.input.value.match(/^.+,\s*|/)[0];
- this.input.value = `${before}${item}${separator}`;
- }
-
- inputListener() {
- const inputId = this.input.getAttribute('id');
- const searchTerm = this.extractLastEntityReference();
-
- if (!(inputId in this.cache)) {
- this.cache[inputId] = {};
- }
-
- if (searchTerm && searchTerm.length > 0) {
- if (this.cache[inputId].hasOwnProperty(searchTerm)) {
- this.suggestionItems = this.cache[inputId][searchTerm];
- this.displayResults();
- this.announceSuggestionCount(this.ul.children.length);
- } else {
- const apiUrl = this.input.getAttribute('data-autocomplete-path');
- this.input.classList.add('ui-autocomplete-loading');
- const xhr = new XMLHttpRequest();
- xhr.open('GET', `${apiUrl}?q=${searchTerm}`);
- xhr.onload = () => {
- this.input.classList.remove('ui-autocomplete-loading');
- if (xhr.status === 200) {
- const results = JSON.parse(xhr.response);
- this.suggestionItems = results;
- this.displayResults();
-
- this.cache[inputId][searchTerm] = results;
- this.announceSuggestionCount(results.length);
- }
- };
- xhr.send();
- }
- }
- }
-
- displayResults() {
- const { announce } = Drupal;
- const typed = this.extractLastEntityReference();
- if (
- typed.length >= this.options.minChars &&
- this.suggestionItems.length > 0
- ) {
- this.ul.innerHTML = '';
- this.suggestions = this.suggestionItems.filter((item) =>
- this.filterResults(item, typed),
- );
-
- if (this.sort !== false) {
- this.sortSuggestions();
- }
-
- this.suggestions = this.suggestions.slice(0, this.options.maxItems);
-
- this.suggestions.forEach((suggestion, index) => {
- this.ul.appendChild(this.suggestionItem(suggestion, index));
- });
-
- if (this.ul.children.length === 0) {
- this.close();
- announce(Drupal.t('No results found'));
- } else {
- this.open();
- this.announceSuggestionCount(this.ul.children.length);
- }
- } else {
- this.close();
- announce(Drupal.t('No results found'));
- }
- }
-
- /**
- * Sorts the array of suggestions.
- */
- sortSuggestions() {
- this.suggestions.sort((prior, current) =>
- prior.label.toUpperCase() > current.label.toUpperCase() ? 1 : -1,
- );
- }
-
- /**
- * Creates a list item that displays the suggestion.
- *
- * @param {object} suggestion
- * A suggestion based on user input. It is an object with label and value
- * properties.
- * @param {number} itemIndex
- * The index of the item.
- *
- * @return {HTMLElement}
- * A list item with the suggestion.
- */
- suggestionItem(suggestion, itemIndex) {
- const li = document.createElement('li');
- // Wrapped in an `` for backwards compatibility.
- li.innerHTML = `${suggestion.value.trim()}`;
- li.setAttribute('role', 'option');
- li.setAttribute('tabindex', '-1');
- li.setAttribute('id', `suggestion-${this.count}-${itemIndex}`);
- li.setAttribute('data-drupal-autocomplete-item', itemIndex);
- li.setAttribute('aria-posinset', itemIndex + 1);
- li.setAttribute('aria-selected', 'false');
- li.onblur = (e) => this.blurHandler(e);
-
- return li;
- }
-
- /**
- * Opens the suggestion list.
- */
- open() {
- this.input.setAttribute('aria-expanded', 'true');
- this.ul.removeAttribute('hidden');
- this.isOpened = true;
- this.ul.style.minWidth = `${this.input.offsetWidth - 4}px`;
- if (!this.hasOwnProperty('popper')) {
- this.initPopper();
- } else {
- this.popper.forceUpdate();
- }
- }
-
- /**
- * Closes the suggestion list.
- */
- close() {
- this.input.setAttribute('aria-expanded', 'false');
- this.ul.setAttribute('hidden', '');
- this.isOpened = false;
- }
-
- /**
- * Returns the last value of an multi-value textfield.
- *
- * @return {string}
- * The last value of the input field.
- */
- extractLastEntityReference() {
- return this.splitValues().pop();
- }
-
- /**
- * Helper splitting selections from the autocomplete value.
- *
- * @return {Array}
- * Array of values, split by comma.
- */
- splitValues() {
- const { value } = this.input;
- const result = [];
- let quote = false;
- let current = '';
- const valueLength = value.length;
- for (let i = 0; i < valueLength; i++) {
- const character = value.charAt(i);
- if (character === '"') {
- current += character;
- quote = !quote;
- } else if (character === ',' && !quote) {
- result.push(current.trim());
- current = '';
- } else {
- current += character;
- }
- }
- if (value.length > 0) {
- result.push(current.trim());
- }
- return result;
- }
-
- /**
- * Provides information on suggestion count to screenreaders.
- *
- * @param {number} count
- * The number of results present.
- */
- announceSuggestionCount(count) {
- const { announce, formatPlural } = Drupal;
- const { maxItems } = this.options;
-
- // If the number of suggestions provided equals the maximum allowed,
- // provide a different message so users are aware there may be additional
- // suggestions that match their criteria.
- const pluralMessage =
- maxItems === count
- ? 'There are at least @count results available. Type additional characters to refine your search.'
- : 'There are @count results available.';
- const message = formatPlural(
- count,
- 'There is one result available.',
- pluralMessage,
- );
-
- announce(Drupal.t(message), 'assertive');
- }
-
- /**
- * Determines if a suggestion should be an available option.
- *
- * @param {object} suggestion
- * A suggestion based on user input. It is an object with label and value
- * properties.
- * @param {string} typed
- * The text entered in the input field.
- *
- * @return {boolean}
- * If the suggestion should be displayed in the results.
- */
- filterResults(suggestion, typed) {
- const { firstCharacterDenylist } = this.options;
- const cardinality = this.getCardinality();
- const suggestionValue = suggestion.value;
- const currentValues = this.splitValues();
-
- const inputSpecificFirstCharDenylist = this.input.hasAttribute(
- 'data-autocomplete-first-character-denylist',
- )
- ? this.input.getAttribute('data-autocomplete-first-character-denylist')
- : '';
-
- // Prevent suggestions if the first input character is in the denylist, if
- // the suggestion has already been added to the field, or if the maximum
- // number of items have been reached.
- if (
- firstCharacterDenylist.indexOf(typed[0]) !== -1 ||
- inputSpecificFirstCharDenylist.indexOf(typed[0]) !== -1 ||
- currentValues.indexOf(suggestionValue) !== -1 ||
- (cardinality > 0 && currentValues.length > cardinality)
- ) {
- return false;
- }
-
- return RegExp(
- this.extractLastEntityReference()
- .trim()
- .replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
- 'i',
- ).test(suggestionValue);
- }
-
- /**
- * Returns the configured cardinality of the field, when available.
- *
- * @return {number}
- * The cardinality of the field. Returns -1 if it is not explicitly
- * configured, which is interpreted as unlimited.
- */
- getCardinality() {
- const wrapper = this.input.closest('[data-autocomplete-cardinality]');
- return wrapper
- ? wrapper.getAttribute('data-autocomplete-cardinality')
- : -1;
- }
-
- /**
- * Initialize positioning of items with PopperJS.
- */
- initPopper() {
- this.popper = Popper.createPopper(this.input, this.ul, {
- placement: 'bottom-start',
- modifiers: [
- {
- name: 'flip',
- options: {
- fallbackPlacements: [],
- },
- },
- ],
- });
- }
- };
-
- Drupal.Autocomplete.instances = [];
-
- /**
- * Attaches the autocomplete behavior to all required fields.
- *
- * @type {Drupal~behavior}
- *
- * @prop {Drupal~behaviorAttach} attach
- * Attaches the autocomplete behaviors.
- */
- Drupal.behaviors.autocomplete = {
- attach(context) {
- const options = drupalSettings.autocompleteOptions;
-
- const $autoCompleteInputs = $(context)
- .find('input.form-autocomplete')
- .once('autocomplete-init');
-
- Drupal.Autocomplete.instances = Drupal.Autocomplete.instances.concat(
- $autoCompleteInputs
- .map(
- (index, autocompleteInput) =>
- new Drupal.Autocomplete(autocompleteInput, options),
- )
- .get(),
- );
- },
- };
-})(jQuery, Drupal, drupalSettings, Popper);
diff --git a/core/modules/d9_autocomplete/js/autocomplete.js b/core/modules/d9_autocomplete/js/autocomplete.js
deleted file mode 100644
index cd73418ec2..0000000000
--- a/core/modules/d9_autocomplete/js/autocomplete.js
+++ /dev/null
@@ -1,421 +0,0 @@
-/**
-* DO NOT EDIT THIS FILE.
-* See the following change record for more information,
-* https://www.drupal.org/node/2815083
-* @preserve
-**/
-
-function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
-
-function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
-
-function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
-
-function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
-
-function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
-
-(function ($, Drupal, drupalSettings) {
- Drupal.Autocomplete = function () {
- function _class(input) {
- var _this = this;
-
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-
- _classCallCheck(this, _class);
-
- this.keyCode = Object.freeze({
- TAB: 9,
- RETURN: 13,
- ESC: 27,
- SPACE: 32,
- PAGEUP: 33,
- PAGEDOWN: 34,
- END: 35,
- HOME: 36,
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40
- });
- this.count = document.querySelectorAll('[data-drupal-autocomplete-initialized]').length;
- var listboxId = "autocomplete-listbox-".concat(this.count);
- var defaultOptions = {
- firstCharacterDenylist: ',',
- minChars: 1,
- maxItems: 10,
- sort: false
- };
- this.options = _objectSpread(_objectSpread({}, defaultOptions), options);
- this.preventClose = false;
- this.isOpened = false;
- this.cache = [];
- this.suggestionItems = [];
- this.hasAnnouncedOnce = false;
- this.input = input;
- this.input.setAttribute('data-drupal-autocomplete-initialized', '');
- this.input.setAttribute('aria-autocomplete', 'list');
- this.input.setAttribute('autocomplete', 'off');
- this.input.setAttribute('data-drupal-autocomplete-input', '');
- this.input.setAttribute('aria-owns', listboxId);
- this.input.setAttribute('role', 'combobox');
- this.input.setAttribute('aria-expanded', 'false');
- this.ul = document.createElement('ul');
- this.ul.setAttribute('role', 'listbox');
- this.ul.setAttribute('data-drupal-autocomplete-list', '');
- this.ul.setAttribute('id', listboxId);
- this.ul.setAttribute('hidden', '');
- this.input.parentNode.appendChild(this.ul);
- this.input.classList.add('ui-autocomplete-input');
- this.ul.classList.add('ui-autocomplete');
- $(this.input).on('input', function () {
- return _this.inputListener();
- });
- $(this.input).on('blur', function (e) {
- return _this.blurHandler(e);
- });
- $(this.input).on('keydown', function (e) {
- return _this.inputKeyDown(e);
- });
- $(this.ul).on('mousedown', function (e) {
- return e.preventDefault();
- });
- $(this.ul).on('click', function (e) {
- return _this.itemClick(e);
- });
- $(this.ul).on('keydown', function (e) {
- return _this.listKeyDown(e);
- });
- $(this.ul).on('blur', function (e) {
- return _this.blurHandler(e);
- });
- }
-
- _createClass(_class, [{
- key: "blurHandler",
- value: function blurHandler(e) {
- if (this.preventClose) {
- this.preventClose = false;
- e.preventDefault();
- } else {
- this.close();
- }
- }
- }, {
- key: "listKeyDown",
- value: function listKeyDown(e) {
- if (!this.ul.contains(document.activeElement) || e.ctrlKey || e.altKey || e.metaKey || e.keyCode === this.keyCode.TAB) {
- return;
- }
-
- switch (e.keyCode) {
- case this.keyCode.SPACE:
- case this.keyCode.RETURN:
- this.replaceInputValue(document.activeElement.textContent);
- this.close();
- this.input.focus();
- break;
-
- case this.keyCode.ESC:
- case this.keyCode.TAB:
- this.input.focus();
- this.close();
- break;
-
- case this.keyCode.UP:
- this.focusPrev();
- break;
-
- case this.keyCode.DOWN:
- this.focusNext();
- break;
-
- default:
- break;
- }
-
- e.stopPropagation();
- e.preventDefault();
- }
- }, {
- key: "focusPrev",
- value: function focusPrev() {
- this.preventClose = true;
- var currentItem = document.activeElement.getAttribute('data-drupal-autocomplete-item');
- var prevIndex = parseInt(currentItem, 10) - 1;
- var previousItem = this.ul.querySelector("[data-drupal-autocomplete-item=\"".concat(prevIndex, "\"]"));
-
- if (previousItem) {
- previousItem.focus();
- }
- }
- }, {
- key: "focusNext",
- value: function focusNext() {
- this.preventClose = true;
- var currentItem = document.activeElement.getAttribute('data-drupal-autocomplete-item');
- var nextIndex = parseInt(currentItem, 10) + 1;
- var nextItem = this.ul.querySelector("[data-drupal-autocomplete-item=\"".concat(nextIndex, "\"]"));
-
- if (nextItem) {
- nextItem.focus();
- }
- }
- }, {
- key: "inputKeyDown",
- value: function inputKeyDown(e) {
- var keyCode = e.keyCode;
-
- if (this.isOpened) {
- if (keyCode === this.keyCode.ESC) {
- this.close();
- }
-
- if (keyCode === this.keyCode.DOWN) {
- e.preventDefault();
- this.preventClose = true;
- this.ul.querySelector('li').focus();
- }
- }
- }
- }, {
- key: "itemClick",
- value: function itemClick(e) {
- var li = e.target;
-
- if (li && e.button === 0) {
- this.replaceInputValue(li.textContent);
- e.preventDefault();
- this.close();
- }
- }
- }, {
- key: "replaceInputValue",
- value: function replaceInputValue(item) {
- var cardinality = this.getCardinality();
- var numItems = this.splitValues().length;
- var separator = numItems < cardinality || parseInt(cardinality, 10) === 0 ? ',' : '';
- var before = this.input.value.match(/^.+,\s*|/)[0];
- this.input.value = "".concat(before).concat(item).concat(separator);
- }
- }, {
- key: "inputListener",
- value: function inputListener() {
- var _this2 = this;
-
- var inputId = this.input.getAttribute('id');
- var searchTerm = this.extractLastEntityReference();
-
- if (!(inputId in this.cache)) {
- this.cache[inputId] = {};
- }
-
- if (searchTerm && searchTerm.length > 0) {
- if (this.cache[inputId].hasOwnProperty(searchTerm)) {
- this.suggestionItems = this.cache[inputId][searchTerm];
- this.displayResults();
- this.announceSuggestionCount(this.ul.children.length);
- } else {
- var apiUrl = this.input.getAttribute('data-autocomplete-path');
- this.input.classList.add('ui-autocomplete-loading');
- var xhr = new XMLHttpRequest();
- xhr.open('GET', "".concat(apiUrl, "?q=").concat(searchTerm));
-
- xhr.onload = function () {
- _this2.input.classList.remove('ui-autocomplete-loading');
-
- if (xhr.status === 200) {
- var results = JSON.parse(xhr.response);
- _this2.suggestionItems = results;
-
- _this2.displayResults();
-
- _this2.cache[inputId][searchTerm] = results;
-
- _this2.announceSuggestionCount(results.length);
- }
- };
-
- xhr.send();
- }
- }
- }
- }, {
- key: "displayResults",
- value: function displayResults() {
- var _this3 = this;
-
- var announce = Drupal.announce;
- var typed = this.extractLastEntityReference();
-
- if (typed.length >= this.options.minChars && this.suggestionItems.length > 0) {
- this.ul.innerHTML = '';
- this.suggestions = this.suggestionItems.filter(function (item) {
- return _this3.filterResults(item, typed);
- });
-
- if (this.sort !== false) {
- this.sortSuggestions();
- }
-
- this.suggestions = this.suggestions.slice(0, this.options.maxItems);
- this.suggestions.forEach(function (suggestion, index) {
- _this3.ul.appendChild(_this3.suggestionItem(suggestion, index));
- });
-
- if (this.ul.children.length === 0) {
- this.close();
- announce(Drupal.t('No results found'));
- } else {
- this.open();
- this.announceSuggestionCount(this.ul.children.length);
- }
- } else {
- this.close();
- announce(Drupal.t('No results found'));
- }
- }
- }, {
- key: "sortSuggestions",
- value: function sortSuggestions() {
- this.suggestions.sort(function (prior, current) {
- return prior.label.toUpperCase() > current.label.toUpperCase() ? 1 : -1;
- });
- }
- }, {
- key: "suggestionItem",
- value: function suggestionItem(suggestion, itemIndex) {
- var _this4 = this;
-
- var li = document.createElement('li');
- li.innerHTML = "".concat(suggestion.value.trim(), "");
- li.setAttribute('role', 'option');
- li.setAttribute('tabindex', '-1');
- li.setAttribute('id', "suggestion-".concat(this.count, "-").concat(itemIndex));
- li.setAttribute('data-drupal-autocomplete-item', itemIndex);
- li.setAttribute('aria-posinset', itemIndex + 1);
- li.setAttribute('aria-selected', 'false');
-
- li.onblur = function (e) {
- return _this4.blurHandler(e);
- };
-
- return li;
- }
- }, {
- key: "open",
- value: function open() {
- this.input.setAttribute('aria-expanded', 'true');
- this.ul.removeAttribute('hidden');
- this.isOpened = true;
- this.ul.style.minWidth = "".concat(this.input.offsetWidth - 4, "px");
-
- if (!this.hasOwnProperty('popper')) {
- this.initPopper();
- } else {
- this.popper.forceUpdate();
- }
- }
- }, {
- key: "close",
- value: function close() {
- this.input.setAttribute('aria-expanded', 'false');
- this.ul.setAttribute('hidden', '');
- this.isOpened = false;
- }
- }, {
- key: "extractLastEntityReference",
- value: function extractLastEntityReference() {
- return this.splitValues().pop();
- }
- }, {
- key: "splitValues",
- value: function splitValues() {
- var value = this.input.value;
- var result = [];
- var quote = false;
- var current = '';
- var valueLength = value.length;
-
- for (var i = 0; i < valueLength; i++) {
- var character = value.charAt(i);
-
- if (character === '"') {
- current += character;
- quote = !quote;
- } else if (character === ',' && !quote) {
- result.push(current.trim());
- current = '';
- } else {
- current += character;
- }
- }
-
- if (value.length > 0) {
- result.push(current.trim());
- }
-
- return result;
- }
- }, {
- key: "announceSuggestionCount",
- value: function announceSuggestionCount(count) {
- var announce = Drupal.announce,
- formatPlural = Drupal.formatPlural;
- var maxItems = this.options.maxItems;
- var pluralMessage = maxItems === count ? 'There are at least @count results available. Type additional characters to refine your search.' : 'There are @count results available.';
- var message = formatPlural(count, 'There is one result available.', pluralMessage);
- announce(Drupal.t(message), 'assertive');
- }
- }, {
- key: "filterResults",
- value: function filterResults(suggestion, typed) {
- var firstCharacterDenylist = this.options.firstCharacterDenylist;
- var cardinality = this.getCardinality();
- var suggestionValue = suggestion.value;
- var currentValues = this.splitValues();
- var inputSpecificFirstCharDenylist = this.input.hasAttribute('data-autocomplete-first-character-denylist') ? this.input.getAttribute('data-autocomplete-first-character-denylist') : '';
-
- if (firstCharacterDenylist.indexOf(typed[0]) !== -1 || inputSpecificFirstCharDenylist.indexOf(typed[0]) !== -1 || currentValues.indexOf(suggestionValue) !== -1 || cardinality > 0 && currentValues.length > cardinality) {
- return false;
- }
-
- return RegExp(this.extractLastEntityReference().trim().replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'), 'i').test(suggestionValue);
- }
- }, {
- key: "getCardinality",
- value: function getCardinality() {
- var wrapper = this.input.closest('[data-autocomplete-cardinality]');
- return wrapper ? wrapper.getAttribute('data-autocomplete-cardinality') : -1;
- }
- }, {
- key: "initPopper",
- value: function initPopper() {
- this.popper = Popper.createPopper(this.input, this.ul, {
- placement: 'bottom-start',
- modifiers: [{
- name: 'flip',
- options: {
- fallbackPlacements: []
- }
- }]
- });
- }
- }]);
-
- return _class;
- }();
-
- Drupal.Autocomplete.instances = [];
- Drupal.behaviors.autocomplete = {
- attach: function attach(context) {
- var options = drupalSettings.autocompleteOptions;
- var $autoCompleteInputs = $(context).find('input.form-autocomplete').once('autocomplete-init');
- Drupal.Autocomplete.instances = Drupal.Autocomplete.instances.concat($autoCompleteInputs.map(function (index, autocompleteInput) {
- return new Drupal.Autocomplete(autocompleteInput, options);
- }).get());
- }
- };
-})(jQuery, Drupal, drupalSettings, Popper);
\ No newline at end of file
diff --git a/core/modules/d9_autocomplete/js/drupalautocomplete.es6.js b/core/modules/d9_autocomplete/js/drupalautocomplete.es6.js
new file mode 100644
index 0000000000..a3ab04f2ca
--- /dev/null
+++ b/core/modules/d9_autocomplete/js/drupalautocomplete.es6.js
@@ -0,0 +1,694 @@
+/**
+ * @file
+ * Standalone autocomplete.
+ */
+
+/**
+ * Constructs a new instance of the DrupalAutocomplete class.
+ *
+ * This adds autocomplete functionality to a text input.
+ *
+ * @param {HTMLElement} input
+ * The element to be used as an autocomplete.
+ *
+ * @return {DrupalAutocomplete}
+ * Class to manage an input's autocomplete functionality.
+ */
+class DrupalAutocomplete {
+ constructor(input, options = {}) {
+ this.keyCode = Object.freeze({
+ TAB: 9,
+ RETURN: 13,
+ ESC: 27,
+ SPACE: 32,
+ PAGEUP: 33,
+ PAGEDOWN: 34,
+ END: 35,
+ HOME: 36,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ });
+
+ this.input = input;
+
+ this.count = document.querySelectorAll(
+ '[data-drupal-autocomplete-input]',
+ ).length;
+ const listboxId = `autocomplete-listbox-${this.count}`;
+
+ const defaultOptions = {
+ firstCharacterDenylist: ',',
+ minChars: 1,
+ maxItems: 10,
+ sort: false,
+ path: false,
+ list: [],
+ cardinality: 1,
+ inputClass: '',
+ ulClass: '',
+ itemClass: '',
+ loadingClass: 'drupal-autocomplete-loading',
+ separatorChar: ',',
+ liveRegion: true,
+ listZindex: 100,
+ messages: {
+ noResults: 'No results found',
+ moreThanMaxResults:
+ 'There are at least @count results available. Type additional characters to refine your search.',
+ someResults: 'There are @count results available.',
+ oneResult: 'There is one result available.',
+ },
+ };
+
+ this.options = {
+ ...defaultOptions,
+ ...options,
+ ...this.attributesToOptions(),
+ };
+
+ this.preventClose = false;
+ this.isOpened = false;
+ this.cache = [];
+ this.suggestionItems = [];
+ this.hasAnnouncedOnce = false;
+
+ // Create a div that will wrap the input and suggestion list.
+ this.wrapper = document.createElement('div');
+ this.wrapper.setAttribute('data-drupal-autocomplete-wrapper', '');
+ input.parentNode.appendChild(this.wrapper);
+ this.wrapper.appendChild(input);
+
+ // Add attributes to the input.
+ this.input.setAttribute('aria-autocomplete', 'list');
+ this.input.setAttribute('autocomplete', 'off');
+ this.input.setAttribute('data-drupal-autocomplete-input', '');
+ this.input.setAttribute('aria-owns', listboxId);
+ this.input.setAttribute('role', 'combobox');
+ this.input.setAttribute('aria-expanded', 'false');
+ if (this.options.inputClass.length > 0) {
+ this.options.inputClass
+ .split(' ')
+ .forEach((className) => this.input.classList.add(className));
+ }
+
+ // Create the list that will display suggestions.
+ this.ul = document.createElement('ul');
+ this.ul.setAttribute('role', 'listbox');
+ this.ul.setAttribute('data-drupal-autocomplete-list', '');
+ this.ul.setAttribute('id', listboxId);
+ this.ul.setAttribute('hidden', '');
+ this.input.parentNode.appendChild(this.ul);
+ if (this.options.ulClass.length > 0) {
+ this.options.ulClass
+ .split(' ')
+ .forEach((className) => this.ul.classList.add(className));
+ }
+
+ // When applicable, create a live region for announcing suggestion results
+ // to assistive technology.
+ this.liveRegion = null;
+ if (this.options.liveRegion === true) {
+ this.liveRegion = document.createElement('span');
+ this.liveRegion.setAttribute('data-drupal-autocomplete-live-region', '');
+ this.liveRegion.setAttribute('aria-live', 'assertive');
+ this.input.parentNode.appendChild(this.liveRegion);
+ }
+
+ // If the liveRegion option is a string, it should be a selector for an
+ // already-existing live region.
+ if (typeof this.options.liveRegion === 'string') {
+ this.liveRegion = document.querySelector(this.options.liveRegion);
+ }
+
+ // Events to add.
+ this.events = {
+ input: {
+ input: () => this.inputListener(),
+ blur: (e) => this.blurHandler(e),
+ keydown: (e) => this.inputKeyDown(e),
+ },
+ ul: {
+ mousedown: (e) => e.preventDefault(),
+ click: (e) => this.itemClick(e),
+ keydown: (e) => this.listKeyDown(e),
+ blur: (e) => this.blurHandler(e),
+ },
+ };
+
+ // If jQuery is available, use it to add events listeners. Otherwise use
+ // vanilla JavaScript.
+ if (window.jQuery) {
+ const $ = window.jQuery;
+ $(this.input).on(this.events.input);
+ $(this.ul).on(this.events.ul);
+ } else {
+ Object.keys(this.events).forEach((elementName) => {
+ Object.keys(this.events[elementName]).forEach((eventName) => {
+ this[elementName].addEventListener(
+ eventName,
+ this.events[elementName][eventName],
+ );
+ });
+ });
+ }
+
+ this.triggerEvent('autocomplete-created');
+ }
+
+ /**
+ * Converts data-autocomplete* attributes into options.
+ *
+ * @return {object} an autocomplete options object.
+ */
+ attributesToOptions() {
+ const options = {};
+ // Any options provided in the `data-autocomplete` attribute will take
+ // precedence over those specified in `data-autocomplete-(x)`.
+ const dataAutocompleteAttributeOptions =
+ this.input.getAttribute('data-autocomplete') || {};
+
+ // Loop through all of the input's attributes. Any attributes beginning with
+ // `data-autocomplete` will be added to an options object.
+ for (let i = 0; i < this.input.attributes.length; i++) {
+ if (
+ this.input.attributes[i].nodeName.includes('data-autocomplete') &&
+ this.input.attributes[i].nodeName !== 'data-autocomplete'
+ ) {
+ // Convert the data attribute name to camel case for use in the options
+ // object.
+ let optionName = this.input.attributes[i].nodeName
+ .replace('data-autocomplete-', '')
+ .split('-')
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
+ .join('');
+ optionName = optionName.charAt(0).toLowerCase() + optionName.slice(1);
+ options[optionName] = this.input.attributes[i].nodeValue;
+ }
+ }
+ return { ...options, ...dataAutocompleteAttributeOptions };
+ }
+
+ /**
+ * Handles blur events.
+ *
+ * @param {Event} e
+ * The blur event.
+ */
+ blurHandler(e) {
+ if (this.preventClose) {
+ this.preventClose = false;
+ e.preventDefault();
+ } else {
+ this.close();
+ }
+ }
+
+ /**
+ * Handles keydown events on the item list.
+ *
+ * @param {Event} e
+ * The keydown event.
+ */
+ listKeyDown(e) {
+ if (
+ !this.ul.contains(document.activeElement) ||
+ e.ctrlKey ||
+ e.altKey ||
+ e.metaKey ||
+ e.keyCode === this.keyCode.TAB
+ ) {
+ return;
+ }
+
+ switch (e.keyCode) {
+ case this.keyCode.SPACE:
+ case this.keyCode.RETURN:
+ this.replaceInputValue(document.activeElement.textContent);
+ this.close();
+ this.input.focus();
+ break;
+
+ case this.keyCode.ESC:
+ case this.keyCode.TAB:
+ this.input.focus();
+ this.close();
+ break;
+
+ case this.keyCode.UP:
+ this.focusPrev();
+ break;
+
+ case this.keyCode.DOWN:
+ this.focusNext();
+ break;
+
+ default:
+ break;
+ }
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ /**
+ * Moves focus to the previous list item.
+ */
+ focusPrev() {
+ this.preventClose = true;
+ const currentItem = document.activeElement.getAttribute(
+ 'data-drupal-autocomplete-item',
+ );
+ const prevIndex = parseInt(currentItem, 10) - 1;
+ const previousItem = this.ul.querySelector(
+ `[data-drupal-autocomplete-item="${prevIndex}"]`,
+ );
+
+ if (previousItem) {
+ this.preventClose = true;
+ previousItem.focus();
+ } else {
+ this.input.focus();
+ this.close();
+ }
+ }
+
+ /**
+ * Moves focus to the next list item.
+ */
+ focusNext() {
+ this.preventClose = true;
+ const currentItem = document.activeElement.getAttribute(
+ 'data-drupal-autocomplete-item',
+ );
+ const nextIndex = parseInt(currentItem, 10) + 1;
+ const nextItem = this.ul.querySelector(
+ `[data-drupal-autocomplete-item="${nextIndex}"]`,
+ );
+ if (nextItem) {
+ this.preventClose = true;
+ nextItem.focus();
+ }
+ }
+
+ /**
+ * Handles keydown events on the autocomplete input.
+ *
+ * @param {Event} e
+ * The keydown event.
+ */
+ inputKeyDown(e) {
+ const { keyCode } = e;
+ if (this.isOpened) {
+ if (keyCode === this.keyCode.ESC) {
+ this.close();
+ }
+ if (keyCode === this.keyCode.DOWN) {
+ e.preventDefault();
+ this.preventClose = true;
+ this.ul.querySelector('li').focus();
+ }
+ }
+ }
+
+ /**
+ * Handles click events on the item list.
+ *
+ * @param {Event} e
+ * The click event.
+ */
+ itemClick(e) {
+ const li = e.target;
+
+ if (li && e.button === 0) {
+ const selected = this.triggerEvent(
+ 'autocomplete-select',
+ {
+ originalEvent: e,
+ selected: li.textContent,
+ },
+ true,
+ );
+
+ if (selected) {
+ this.replaceInputValue(li.textContent);
+ e.preventDefault();
+ this.close();
+ this.triggerEvent('autocomplete-selection-added', {
+ added: li.textContent,
+ });
+ }
+ }
+ }
+
+ /**
+ * Replaces the value of an input field when a new value is chosen.
+ *
+ * @param {string} item
+ * The item being added to the field.
+ */
+ replaceInputValue(item) {
+ const separator = this.separator();
+ const before = this.previousItems(separator);
+ this.input.value = `${before}${item}`;
+ }
+
+ /**
+ * Returns the separator character.
+ *
+ * @return {string}
+ * The separator character or a zero-length string.
+ * If the autocomplete input does not support multiple items or has reached
+ * The maximum number of items that can be added, a zero-length string is
+ * returned as no separator is needed.
+ */
+ separator() {
+ const { cardinality } = this.options;
+ const numItems = this.splitValues().length;
+ return numItems < cardinality || parseInt(cardinality, 10) <= 0
+ ? this.options.separatorChar
+ : '';
+ }
+
+ /**
+ * Gets all existing items in the autocomplete input.
+ *
+ * @param {string} separator
+ * The character separating the items.
+ *
+ * @return {string|string}
+ * The string of existing values in the input.
+ */
+ previousItems(separator) {
+ const regex = new RegExp(`^.+${separator}\\s*|`);
+ const match = this.input.value.match(regex)[0];
+ return match && match.length > 0 ? `${match.trim()} ` : '';
+ }
+
+ /**
+ * Performs a search based on what has been typed in the input.
+ */
+ inputListener() {
+ const inputId = this.input.getAttribute('id');
+ const searchTerm = this.extractLastInputValue();
+
+ if (!(inputId in this.cache)) {
+ this.cache[inputId] = {};
+ }
+
+ if (searchTerm && searchTerm.length > 0) {
+ if (this.cache[inputId].hasOwnProperty(searchTerm)) {
+ this.suggestionItems = this.cache[inputId][searchTerm];
+ this.displayResults();
+ } else if (this.options.path) {
+ this.options.loadingClass
+ .split(' ')
+ .forEach((className) => this.input.classList.add(className));
+ fetch(`${this.options.path}?q=${searchTerm}`)
+ .then((response) => response.json())
+ .then((results) => {
+ this.options.loadingClass
+ .split(' ')
+ .forEach((className) => this.input.classList.remove(className));
+ this.suggestionItems = results;
+ this.displayResults();
+ this.cache[inputId][searchTerm] = results;
+ });
+ } else {
+ this.suggestionItems = this.list;
+ this.displayResults();
+ }
+ } else {
+ this.suggestionItems = this.options.list;
+ this.displayResults();
+ }
+ }
+
+ /**
+ * Displays the results retrieved in inputListener().
+ */
+ displayResults() {
+ const typed = this.extractLastInputValue();
+ this.ul.innerHTML = '';
+ if (
+ typed &&
+ typed.length >= this.options.minChars &&
+ this.suggestionItems.length > 0
+ ) {
+ this.suggestions = this.suggestionItems.filter((item) =>
+ this.filterResults(item, typed),
+ );
+
+ if (this.sort !== false) {
+ this.sortSuggestions();
+ }
+
+ this.suggestions = this.suggestions.slice(0, this.options.maxItems);
+
+ this.suggestions.forEach((suggestion, index) => {
+ this.ul.appendChild(this.suggestionItem(suggestion, index));
+ });
+ }
+
+ if (this.ul.children.length === 0) {
+ this.close();
+ } else {
+ this.open();
+ }
+ this.announceResults(this.ul.children.length);
+ }
+
+ /**
+ * Sorts the array of suggestions.
+ */
+ sortSuggestions() {
+ this.suggestions.sort((prior, current) =>
+ prior.label.toUpperCase() > current.label.toUpperCase() ? 1 : -1,
+ );
+ }
+
+ /**
+ * Creates a list item that displays the suggestion.
+ *
+ * @param {object} suggestion
+ * A suggestion based on user input. It is an object with label and value
+ * properties.
+ * @param {number} itemIndex
+ * The index of the item.
+ *
+ * @return {HTMLElement}
+ * A list item with the suggestion.
+ */
+ suggestionItem(suggestion, itemIndex) {
+ const li = document.createElement('li');
+ li.innerHTML = this.constructor.formatSuggestionItem(suggestion);
+ if (this.options.itemClass.length > 0) {
+ this.options.itemClass
+ .split(' ')
+ .forEach((className) => li.classList.add(className));
+ }
+ li.setAttribute('role', 'option');
+ li.setAttribute('tabindex', '-1');
+ li.setAttribute('id', `suggestion-${this.count}-${itemIndex}`);
+ li.setAttribute('data-drupal-autocomplete-item', itemIndex);
+ li.setAttribute('aria-posinset', itemIndex + 1);
+ li.setAttribute('aria-selected', 'false');
+ li.onblur = (e) => this.blurHandler(e);
+
+ return li;
+ }
+
+ /**
+ * Formats how a suggestion is structured in the suggestion list.
+ *
+ * @param {object} suggestion
+ * Object with value and label properties.
+ *
+ * @return {string}
+ * The text and html of a suggestion item.
+ */
+ static formatSuggestionItem(suggestion) {
+ return suggestion.value.trim();
+ }
+
+ /**
+ * Opens the suggestion list.
+ */
+ open() {
+ this.input.setAttribute('aria-expanded', 'true');
+ this.ul.removeAttribute('hidden');
+ this.ul.style.zIndex = this.options.listZindex;
+ this.isOpened = true;
+ this.ul.style.minWidth = `${this.input.offsetWidth - 4}px`;
+ this.triggerEvent('autocomplete-open');
+ }
+
+ /**
+ * Closes the suggestion list.
+ */
+ close() {
+ this.input.setAttribute('aria-expanded', 'false');
+ this.ul.setAttribute('hidden', '');
+ this.isOpened = false;
+ this.triggerEvent('autocomplete-close');
+ }
+
+ /**
+ * Returns the last value of an multi-value textfield.
+ *
+ * @return {string}
+ * The last value of the input field.
+ */
+ extractLastInputValue() {
+ return this.splitValues().pop();
+ }
+
+ /**
+ * Helper splitting selections from the autocomplete value.
+ *
+ * @return {Array}
+ * Array of values, split by comma.
+ */
+ splitValues() {
+ const { value } = this.input;
+ const result = [];
+ let quote = false;
+ let current = '';
+ const valueLength = value.length;
+ for (let i = 0; i < valueLength; i++) {
+ const character = value.charAt(i);
+ if (character === '"') {
+ current += character;
+ quote = !quote;
+ } else if (character === ',' && !quote) {
+ result.push(current.trim());
+ current = '';
+ } else {
+ current += character;
+ }
+ }
+ if (value.length > 0) {
+ result.push(current.trim());
+ }
+ return result;
+ }
+
+ /**
+ * Determines if a suggestion should be an available option.
+ *
+ * @param {object} suggestion
+ * A suggestion based on user input. It is an object with label and value
+ * properties.
+ * @param {string} typed
+ * The text entered in the input field.
+ *
+ * @return {boolean}
+ * If the suggestion should be displayed in the results.
+ */
+ filterResults(suggestion, typed) {
+ const { firstCharacterDenylist, cardinality } = this.options;
+ const suggestionValue = suggestion.value;
+ const currentValues = this.splitValues();
+
+ // Prevent suggestions if the first input character is in the denylist, if
+ // the suggestion has already been added to the field, or if the maximum
+ // number of items have been reached.
+ if (
+ firstCharacterDenylist.indexOf(typed[0]) !== -1 ||
+ currentValues.indexOf(suggestionValue) !== -1 ||
+ (cardinality > 0 && currentValues.length > cardinality)
+ ) {
+ return false;
+ }
+
+ return RegExp(
+ this.extractLastInputValue()
+ .trim()
+ .replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
+ 'i',
+ ).test(suggestionValue);
+ }
+
+ /**
+ * Announces number of suggestions found to a assistive tech.
+ * @param {number} count
+ * The number of suggestions.
+ */
+ announceResults(count) {
+ const message = this.resultsMessage(count);
+ if (this.liveRegion) {
+ this.liveRegion.textContent = message;
+ }
+ }
+
+ /**
+ * A message regarding the number of suggestions found.
+ *
+ * @param {number} count
+ * The number of suggestions found.
+ *
+ * @return {string}
+ * A message based on the number of suggestions found.
+ */
+ resultsMessage(count) {
+ const { maxItems } = this.options;
+ let message = '';
+ if (count === 0) {
+ message = this.options.messages.noResults;
+ } else if (maxItems === count) {
+ message = this.options.messages.moreThanMaxResults;
+ } else if (count === 1) {
+ message = this.options.messages.oneResult;
+ } else {
+ message = this.options.messages.someResults;
+ }
+ return message.replace('@count', count);
+ }
+
+ /**
+ * Remove all event listeners added by this class.
+ */
+ destroy() {
+ if (window.jQuery) {
+ const $ = window.jQuery;
+ $(this.input).off(this.events.input);
+ $(this.ul).off(this.events.ul);
+ } else {
+ Object.keys(this.events).forEach((elementName) => {
+ Object.keys(this.events[elementName]).forEach((eventName) => {
+ this[elementName].removeEventListener(
+ eventName,
+ this.events[elementName][eventName],
+ );
+ });
+ });
+ }
+ }
+
+ /**
+ * Dispatches an autocomplete event
+ *
+ * @param {string} type
+ * The event type.
+ * @param {object} additionalData
+ * Additional data attached to the event's `details` property.
+ * @param {boolean} cancelable
+ * If the dispatched event should be cancelable.
+ *
+ * @return {event}
+ * The triggered event.
+ */
+ triggerEvent(type, additionalData = {}, cancelable = false) {
+ const event = new CustomEvent(type, {
+ detail: {
+ autocomplete: this,
+ ...additionalData,
+ },
+ cancelable,
+ });
+ return this.input.dispatchEvent(event);
+ }
+}
+
+window.DrupalAutocomplete = DrupalAutocomplete;
diff --git a/core/modules/d9_autocomplete/js/drupalautocomplete.js b/core/modules/d9_autocomplete/js/drupalautocomplete.js
new file mode 100644
index 0000000000..4511cd9b5a
--- /dev/null
+++ b/core/modules/d9_autocomplete/js/drupalautocomplete.js
@@ -0,0 +1,552 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var DrupalAutocomplete = function () {
+ function DrupalAutocomplete(input) {
+ var _this = this;
+
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ _classCallCheck(this, DrupalAutocomplete);
+
+ this.keyCode = Object.freeze({
+ TAB: 9,
+ RETURN: 13,
+ ESC: 27,
+ SPACE: 32,
+ PAGEUP: 33,
+ PAGEDOWN: 34,
+ END: 35,
+ HOME: 36,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40
+ });
+ this.input = input;
+ this.count = document.querySelectorAll('[data-drupal-autocomplete-input]').length;
+ var listboxId = "autocomplete-listbox-".concat(this.count);
+ var defaultOptions = {
+ firstCharacterDenylist: ',',
+ minChars: 1,
+ maxItems: 10,
+ sort: false,
+ path: false,
+ list: [],
+ cardinality: 1,
+ inputClass: '',
+ ulClass: '',
+ itemClass: '',
+ loadingClass: 'drupal-autocomplete-loading',
+ separatorChar: ',',
+ liveRegion: true,
+ listZindex: 100,
+ messages: {
+ noResults: 'No results found',
+ moreThanMaxResults: 'There are at least @count results available. Type additional characters to refine your search.',
+ someResults: 'There are @count results available.',
+ oneResult: 'There is one result available.'
+ }
+ };
+ this.options = _objectSpread(_objectSpread(_objectSpread({}, defaultOptions), options), this.attributesToOptions());
+ this.preventClose = false;
+ this.isOpened = false;
+ this.cache = [];
+ this.suggestionItems = [];
+ this.hasAnnouncedOnce = false;
+ this.wrapper = document.createElement('div');
+ this.wrapper.setAttribute('data-drupal-autocomplete-wrapper', '');
+ input.parentNode.appendChild(this.wrapper);
+ this.wrapper.appendChild(input);
+ this.input.setAttribute('aria-autocomplete', 'list');
+ this.input.setAttribute('autocomplete', 'off');
+ this.input.setAttribute('data-drupal-autocomplete-input', '');
+ this.input.setAttribute('aria-owns', listboxId);
+ this.input.setAttribute('role', 'combobox');
+ this.input.setAttribute('aria-expanded', 'false');
+
+ if (this.options.inputClass.length > 0) {
+ this.options.inputClass.split(' ').forEach(function (className) {
+ return _this.input.classList.add(className);
+ });
+ }
+
+ this.ul = document.createElement('ul');
+ this.ul.setAttribute('role', 'listbox');
+ this.ul.setAttribute('data-drupal-autocomplete-list', '');
+ this.ul.setAttribute('id', listboxId);
+ this.ul.setAttribute('hidden', '');
+ this.input.parentNode.appendChild(this.ul);
+
+ if (this.options.ulClass.length > 0) {
+ this.options.ulClass.split(' ').forEach(function (className) {
+ return _this.ul.classList.add(className);
+ });
+ }
+
+ this.liveRegion = null;
+
+ if (this.options.liveRegion === true) {
+ this.liveRegion = document.createElement('span');
+ this.liveRegion.setAttribute('data-drupal-autocomplete-live-region', '');
+ this.liveRegion.setAttribute('aria-live', 'assertive');
+ this.input.parentNode.appendChild(this.liveRegion);
+ }
+
+ if (typeof this.options.liveRegion === 'string') {
+ this.liveRegion = document.querySelector(this.options.liveRegion);
+ }
+
+ this.events = {
+ input: {
+ input: function input() {
+ return _this.inputListener();
+ },
+ blur: function blur(e) {
+ return _this.blurHandler(e);
+ },
+ keydown: function keydown(e) {
+ return _this.inputKeyDown(e);
+ }
+ },
+ ul: {
+ mousedown: function mousedown(e) {
+ return e.preventDefault();
+ },
+ click: function click(e) {
+ return _this.itemClick(e);
+ },
+ keydown: function keydown(e) {
+ return _this.listKeyDown(e);
+ },
+ blur: function blur(e) {
+ return _this.blurHandler(e);
+ }
+ }
+ };
+
+ if (window.jQuery) {
+ var $ = window.jQuery;
+ $(this.input).on(this.events.input);
+ $(this.ul).on(this.events.ul);
+ } else {
+ Object.keys(this.events).forEach(function (elementName) {
+ Object.keys(_this.events[elementName]).forEach(function (eventName) {
+ _this[elementName].addEventListener(eventName, _this.events[elementName][eventName]);
+ });
+ });
+ }
+
+ this.triggerEvent('autocomplete-created');
+ }
+
+ _createClass(DrupalAutocomplete, [{
+ key: "attributesToOptions",
+ value: function attributesToOptions() {
+ var options = {};
+ var dataAutocompleteAttributeOptions = this.input.getAttribute('data-autocomplete') || {};
+
+ for (var i = 0; i < this.input.attributes.length; i++) {
+ if (this.input.attributes[i].nodeName.includes('data-autocomplete') && this.input.attributes[i].nodeName !== 'data-autocomplete') {
+ var optionName = this.input.attributes[i].nodeName.replace('data-autocomplete-', '').split('-').map(function (w) {
+ return w.charAt(0).toUpperCase() + w.slice(1);
+ }).join('');
+ optionName = optionName.charAt(0).toLowerCase() + optionName.slice(1);
+ options[optionName] = this.input.attributes[i].nodeValue;
+ }
+ }
+
+ return _objectSpread(_objectSpread({}, options), dataAutocompleteAttributeOptions);
+ }
+ }, {
+ key: "blurHandler",
+ value: function blurHandler(e) {
+ if (this.preventClose) {
+ this.preventClose = false;
+ e.preventDefault();
+ } else {
+ this.close();
+ }
+ }
+ }, {
+ key: "listKeyDown",
+ value: function listKeyDown(e) {
+ if (!this.ul.contains(document.activeElement) || e.ctrlKey || e.altKey || e.metaKey || e.keyCode === this.keyCode.TAB) {
+ return;
+ }
+
+ switch (e.keyCode) {
+ case this.keyCode.SPACE:
+ case this.keyCode.RETURN:
+ this.replaceInputValue(document.activeElement.textContent);
+ this.close();
+ this.input.focus();
+ break;
+
+ case this.keyCode.ESC:
+ case this.keyCode.TAB:
+ this.input.focus();
+ this.close();
+ break;
+
+ case this.keyCode.UP:
+ this.focusPrev();
+ break;
+
+ case this.keyCode.DOWN:
+ this.focusNext();
+ break;
+
+ default:
+ break;
+ }
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }, {
+ key: "focusPrev",
+ value: function focusPrev() {
+ this.preventClose = true;
+ var currentItem = document.activeElement.getAttribute('data-drupal-autocomplete-item');
+ var prevIndex = parseInt(currentItem, 10) - 1;
+ var previousItem = this.ul.querySelector("[data-drupal-autocomplete-item=\"".concat(prevIndex, "\"]"));
+
+ if (previousItem) {
+ this.preventClose = true;
+ previousItem.focus();
+ } else {
+ this.input.focus();
+ this.close();
+ }
+ }
+ }, {
+ key: "focusNext",
+ value: function focusNext() {
+ this.preventClose = true;
+ var currentItem = document.activeElement.getAttribute('data-drupal-autocomplete-item');
+ var nextIndex = parseInt(currentItem, 10) + 1;
+ var nextItem = this.ul.querySelector("[data-drupal-autocomplete-item=\"".concat(nextIndex, "\"]"));
+
+ if (nextItem) {
+ this.preventClose = true;
+ nextItem.focus();
+ }
+ }
+ }, {
+ key: "inputKeyDown",
+ value: function inputKeyDown(e) {
+ var keyCode = e.keyCode;
+
+ if (this.isOpened) {
+ if (keyCode === this.keyCode.ESC) {
+ this.close();
+ }
+
+ if (keyCode === this.keyCode.DOWN) {
+ e.preventDefault();
+ this.preventClose = true;
+ this.ul.querySelector('li').focus();
+ }
+ }
+ }
+ }, {
+ key: "itemClick",
+ value: function itemClick(e) {
+ var li = e.target;
+
+ if (li && e.button === 0) {
+ var selected = this.triggerEvent('autocomplete-select', {
+ originalEvent: e,
+ selected: li.textContent
+ }, true);
+
+ if (selected) {
+ this.replaceInputValue(li.textContent);
+ e.preventDefault();
+ this.close();
+ this.triggerEvent('autocomplete-selection-added', {
+ added: li.textContent
+ });
+ }
+ }
+ }
+ }, {
+ key: "replaceInputValue",
+ value: function replaceInputValue(item) {
+ var separator = this.separator();
+ var before = this.previousItems(separator);
+ this.input.value = "".concat(before).concat(item);
+ }
+ }, {
+ key: "separator",
+ value: function separator() {
+ var cardinality = this.options.cardinality;
+ var numItems = this.splitValues().length;
+ return numItems < cardinality || parseInt(cardinality, 10) <= 0 ? this.options.separatorChar : '';
+ }
+ }, {
+ key: "previousItems",
+ value: function previousItems(separator) {
+ var regex = new RegExp("^.+".concat(separator, "\\s*|"));
+ var match = this.input.value.match(regex)[0];
+ return match && match.length > 0 ? "".concat(match.trim(), " ") : '';
+ }
+ }, {
+ key: "inputListener",
+ value: function inputListener() {
+ var _this2 = this;
+
+ var inputId = this.input.getAttribute('id');
+ var searchTerm = this.extractLastInputValue();
+
+ if (!(inputId in this.cache)) {
+ this.cache[inputId] = {};
+ }
+
+ if (searchTerm && searchTerm.length > 0) {
+ if (this.cache[inputId].hasOwnProperty(searchTerm)) {
+ this.suggestionItems = this.cache[inputId][searchTerm];
+ this.displayResults();
+ } else if (this.options.path) {
+ this.options.loadingClass.split(' ').forEach(function (className) {
+ return _this2.input.classList.add(className);
+ });
+ fetch("".concat(this.options.path, "?q=").concat(searchTerm)).then(function (response) {
+ return response.json();
+ }).then(function (results) {
+ _this2.options.loadingClass.split(' ').forEach(function (className) {
+ return _this2.input.classList.remove(className);
+ });
+
+ _this2.suggestionItems = results;
+
+ _this2.displayResults();
+
+ _this2.cache[inputId][searchTerm] = results;
+ });
+ } else {
+ this.suggestionItems = this.list;
+ this.displayResults();
+ }
+ } else {
+ this.suggestionItems = this.options.list;
+ this.displayResults();
+ }
+ }
+ }, {
+ key: "displayResults",
+ value: function displayResults() {
+ var _this3 = this;
+
+ var typed = this.extractLastInputValue();
+ this.ul.innerHTML = '';
+
+ if (typed && typed.length >= this.options.minChars && this.suggestionItems.length > 0) {
+ this.suggestions = this.suggestionItems.filter(function (item) {
+ return _this3.filterResults(item, typed);
+ });
+
+ if (this.sort !== false) {
+ this.sortSuggestions();
+ }
+
+ this.suggestions = this.suggestions.slice(0, this.options.maxItems);
+ this.suggestions.forEach(function (suggestion, index) {
+ _this3.ul.appendChild(_this3.suggestionItem(suggestion, index));
+ });
+ }
+
+ if (this.ul.children.length === 0) {
+ this.close();
+ } else {
+ this.open();
+ }
+
+ this.announceResults(this.ul.children.length);
+ }
+ }, {
+ key: "sortSuggestions",
+ value: function sortSuggestions() {
+ this.suggestions.sort(function (prior, current) {
+ return prior.label.toUpperCase() > current.label.toUpperCase() ? 1 : -1;
+ });
+ }
+ }, {
+ key: "suggestionItem",
+ value: function suggestionItem(suggestion, itemIndex) {
+ var _this4 = this;
+
+ var li = document.createElement('li');
+ li.innerHTML = this.constructor.formatSuggestionItem(suggestion);
+
+ if (this.options.itemClass.length > 0) {
+ this.options.itemClass.split(' ').forEach(function (className) {
+ return li.classList.add(className);
+ });
+ }
+
+ li.setAttribute('role', 'option');
+ li.setAttribute('tabindex', '-1');
+ li.setAttribute('id', "suggestion-".concat(this.count, "-").concat(itemIndex));
+ li.setAttribute('data-drupal-autocomplete-item', itemIndex);
+ li.setAttribute('aria-posinset', itemIndex + 1);
+ li.setAttribute('aria-selected', 'false');
+
+ li.onblur = function (e) {
+ return _this4.blurHandler(e);
+ };
+
+ return li;
+ }
+ }, {
+ key: "open",
+ value: function open() {
+ this.input.setAttribute('aria-expanded', 'true');
+ this.ul.removeAttribute('hidden');
+ this.ul.style.zIndex = this.options.listZindex;
+ this.isOpened = true;
+ this.ul.style.minWidth = "".concat(this.input.offsetWidth - 4, "px");
+ this.triggerEvent('autocomplete-open');
+ }
+ }, {
+ key: "close",
+ value: function close() {
+ this.input.setAttribute('aria-expanded', 'false');
+ this.ul.setAttribute('hidden', '');
+ this.isOpened = false;
+ this.triggerEvent('autocomplete-close');
+ }
+ }, {
+ key: "extractLastInputValue",
+ value: function extractLastInputValue() {
+ return this.splitValues().pop();
+ }
+ }, {
+ key: "splitValues",
+ value: function splitValues() {
+ var value = this.input.value;
+ var result = [];
+ var quote = false;
+ var current = '';
+ var valueLength = value.length;
+
+ for (var i = 0; i < valueLength; i++) {
+ var character = value.charAt(i);
+
+ if (character === '"') {
+ current += character;
+ quote = !quote;
+ } else if (character === ',' && !quote) {
+ result.push(current.trim());
+ current = '';
+ } else {
+ current += character;
+ }
+ }
+
+ if (value.length > 0) {
+ result.push(current.trim());
+ }
+
+ return result;
+ }
+ }, {
+ key: "filterResults",
+ value: function filterResults(suggestion, typed) {
+ var _this$options = this.options,
+ firstCharacterDenylist = _this$options.firstCharacterDenylist,
+ cardinality = _this$options.cardinality;
+ var suggestionValue = suggestion.value;
+ var currentValues = this.splitValues();
+
+ if (firstCharacterDenylist.indexOf(typed[0]) !== -1 || currentValues.indexOf(suggestionValue) !== -1 || cardinality > 0 && currentValues.length > cardinality) {
+ return false;
+ }
+
+ return RegExp(this.extractLastInputValue().trim().replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'), 'i').test(suggestionValue);
+ }
+ }, {
+ key: "announceResults",
+ value: function announceResults(count) {
+ var message = this.resultsMessage(count);
+
+ if (this.liveRegion) {
+ this.liveRegion.textContent = message;
+ }
+ }
+ }, {
+ key: "resultsMessage",
+ value: function resultsMessage(count) {
+ var maxItems = this.options.maxItems;
+ var message = '';
+
+ if (count === 0) {
+ message = this.options.messages.noResults;
+ } else if (maxItems === count) {
+ message = this.options.messages.moreThanMaxResults;
+ } else if (count === 1) {
+ message = this.options.messages.oneResult;
+ } else {
+ message = this.options.messages.someResults;
+ }
+
+ return message.replace('@count', count);
+ }
+ }, {
+ key: "destroy",
+ value: function destroy() {
+ var _this5 = this;
+
+ if (window.jQuery) {
+ var $ = window.jQuery;
+ $(this.input).off(this.events.input);
+ $(this.ul).off(this.events.ul);
+ } else {
+ Object.keys(this.events).forEach(function (elementName) {
+ Object.keys(_this5.events[elementName]).forEach(function (eventName) {
+ _this5[elementName].removeEventListener(eventName, _this5.events[elementName][eventName]);
+ });
+ });
+ }
+ }
+ }, {
+ key: "triggerEvent",
+ value: function triggerEvent(type) {
+ var additionalData = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var cancelable = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var event = new CustomEvent(type, {
+ detail: _objectSpread({
+ autocomplete: this
+ }, additionalData),
+ cancelable: cancelable
+ });
+ return this.input.dispatchEvent(event);
+ }
+ }], [{
+ key: "formatSuggestionItem",
+ value: function formatSuggestionItem(suggestion) {
+ return suggestion.value.trim();
+ }
+ }]);
+
+ return DrupalAutocomplete;
+}();
+
+window.DrupalAutocomplete = DrupalAutocomplete;
\ No newline at end of file
diff --git a/core/themes/claro/css/components/jquery.ui/theme.css b/core/themes/claro/css/components/jquery.ui/theme.css
index 91c76bc080..b5c5f86461 100644
--- a/core/themes/claro/css/components/jquery.ui/theme.css
+++ b/core/themes/claro/css/components/jquery.ui/theme.css
@@ -623,19 +623,23 @@
text-decoration: none;
}
-.ui-autocomplete .ui-menu-item-wrapper.ui-state-active {
+.ui-autocomplete .ui-menu-item.ui-state-focus,
+.autocomplete .ui-menu-item.ui-state-hover,
+[data-drupal-autocomplete-list] .ui-menu-item-wrapper:hover {
margin: 0;
- color: #fff;
- background: #003cc5;
+ background: #0072b9;
}
-.ui-autocomplete .ui-menu-item.ui-state-focus,
-.autocomplete .ui-menu-item.ui-state-hover {
+.ui-autocomplete .ui-menu-item-wrapper.ui-state-active,
+[data-drupal-autocomplete-list] .ui-menu-item-wrapper:focus {
margin: 0;
- background: #0072b9;
+ color: #fff;
+ background: #003cc5;
+ box-shadow: none;
}
.ui-autocomplete .ui-state-focus a,
-.autocomplete .ui-state-hover a {
+.autocomplete .ui-state-hover a,
+[data-drupal-autocomplete-list] .ui-menu-item-wrapper:hover {
color: #fff;
}
diff --git a/core/themes/claro/css/components/jquery.ui/theme.pcss.css b/core/themes/claro/css/components/jquery.ui/theme.pcss.css
index 9d0f877635..123820aca9 100644
--- a/core/themes/claro/css/components/jquery.ui/theme.pcss.css
+++ b/core/themes/claro/css/components/jquery.ui/theme.pcss.css
@@ -404,17 +404,22 @@
.ui-autocomplete .ui-menu-item-wrapper:hover {
text-decoration: none;
}
-.ui-autocomplete .ui-menu-item-wrapper.ui-state-active {
- margin: 0;
- color: var(--jui-dropdown--active-fg-color);
- background: var(--jui-dropdown--active-bg-color);
-}
.ui-autocomplete .ui-menu-item.ui-state-focus,
-.autocomplete .ui-menu-item.ui-state-hover {
+.autocomplete .ui-menu-item.ui-state-hover,
+[data-drupal-autocomplete-list] .ui-menu-item:hover {
margin: 0;
background: #0072b9;
}
+.ui-autocomplete .ui-menu-item-wrapper.ui-state-active,
+[data-drupal-autocomplete-list] .ui-menu-item:focus {
+ margin: 0;
+ color: var(--jui-dropdown--active-fg-color);
+ background: var(--jui-dropdown--active-bg-color);
+ box-shadow: none;
+}
+
.ui-autocomplete .ui-state-focus a,
-.autocomplete .ui-state-hover a {
+.autocomplete .ui-state-hover a,
+[data-drupal-autocomplete-list] .ui-menu-item:hover {
color: #fff;
}