diff --git a/src/pat/relateditems/relateditems.js b/src/pat/relateditems/relateditems.js index 203664aa8..dad490755 100644 --- a/src/pat/relateditems/relateditems.js +++ b/src/pat/relateditems/relateditems.js @@ -4,7 +4,6 @@ import Base from "@patternslib/patternslib/src/core/base"; import _t from "../../core/i18n-wrapper"; import utils from "../../core/utils"; import registry from "@patternslib/patternslib/src/core/registry"; -import Select2 from "../select2/select2"; const KEY = { LEFT: 37, @@ -501,6 +500,9 @@ export default Base.extend({ }, async init() { + // select2 methods are borrowed onto this instance; load them lazily so + // the select2 body stays out of the eager patterns chunk. + this._select2 = (await import("../select2/select2--implementation")).default; (await import("bootstrap")).Dropdown; import("./relateditems.scss"); @@ -525,8 +527,8 @@ export default Base.extend({ this.$el.wrap('
'); this.$container = this.$el.parents(".pat-relateditems-container"); - Select2.prototype.initializeValues.call(this); - Select2.prototype.initializeTags.call(this); + this._select2.initializeValues.call(this); + this._select2.initializeTags.call(this); this.options.formatSelection = (item) => { item = $.extend( @@ -674,8 +676,8 @@ export default Base.extend({ this.options.id = (item) => item.UID; - await Select2.prototype.initializeSelect2.call(this); - await Select2.prototype.initializeOrdering.call(this); + await this._select2.initializeSelect2.call(this); + await this._select2.initializeOrdering.call(this); this.$toolbar = $('
'); this.$container.prepend(this.$toolbar); @@ -697,7 +699,7 @@ export default Base.extend({ }); $(document).on("keyup", this.$el, (event) => { - const isOpen = Select2.prototype.opened.call(this); + const isOpen = this._select2.opened.call(this); if (!isOpen) { return; } diff --git a/src/pat/select2/select2--implementation.js b/src/pat/select2/select2--implementation.js new file mode 100644 index 000000000..18cdb5d4f --- /dev/null +++ b/src/pat/select2/select2--implementation.js @@ -0,0 +1,312 @@ +import $ from "jquery"; +import events from "@patternslib/patternslib/src/core/events"; +import I18n from "../../core/i18n"; +import utils from "../../core/utils"; + +// Pattern config object — grafted onto the thin registered pattern in +// select2.js and run there. The select2 library, its CSS and the +// select2_locale_* context all load from here, keeping them (and this +// wrapper) out of the eager patterns chunk. pat-relateditems borrows +// these methods via a lazy import of this module. +export default { + defaults: { + separator: ",", + ajaxTimeout: 300, + }, + + initializeValues() { + // Init Selection --------------------------------------------- + if (this.options.initialValues) { + this.options.id = (term) => { + return term.id; + }; + this.options.initSelection = ($el, callback) => { + const data = []; + const value = $el.val(); + let seldefaults = this.options.initialValues; + + // Create the initSelection value that contains the default selection, + // but in a javascript object + if ( + typeof this.options.initialValues === "string" && + this.options.initialValues !== "" + ) { + // if default selection value starts with a '{', then treat the value as + // a JSON object that needs to be parsed + if (this.options.initialValues[0] === "{") { + seldefaults = JSON.parse(this.options.initialValues); + } + // otherwise, treat the value as a list, separated by the defaults.separator value of + // strings in the format "id:text", and convert it to an object + else { + seldefaults = {}; + const initial_values = $( + this.options.initialValues.split(this.options.separator) + ); + for (const it of initial_values) { + const selection = it.split(":"); + const id = selection[0].trim(); + const text = selection[1].trim(); + seldefaults[id] = text; + } + } + } + + const items = $(value.split(this.options.separator)); + for (const it of items) { + let text = it; + if (seldefaults[it]) { + text = seldefaults[it]; + } + data.push({ + id: utils.removeHTML(it), + text: utils.removeHTML(text), + }); + } + callback(data); + }; + } + }, + + initializeTags() { + if (this.options.tags && typeof this.options.tags === "string") { + if (this.options.tags.substr(0, 1) === "[") { + this.options.tags = JSON.parse(this.options.tags); + } else { + this.options.tags = this.options.tags.split(this.options.separator); + } + } + + if (this.options.tags && !this.options.allowNewItems) { + this.options.data = this.options.tags.map((value) => { + return { id: value, text: value }; + }); + this.options.multiple = true; + delete this.options.tags; + } + }, + + async initializeOrdering() { + if (!this.options.orderable) { + return; + } + const Sortable = (await import("sortablejs")).default; + const _initializeOrdering = () => { + const sortable_el = this.$select2[0].querySelector(".select2-choices"); + new Sortable(sortable_el, { + draggable: "li", + dragClass: "select2-choice-dragging", + chosenClass: "dragging", + onStart: () => this.$el.select2("onSortStart"), + onEnd: () => this.$el.select2("onSortEnd"), + }); + }; + this.$el.on("change", _initializeOrdering.bind(this)); + _initializeOrdering(); + }, + + async initializeSelect2() { + import("select2/select2.css"); + import("./select2.scss"); + await import("select2"); + try { + // Don't load "en" which is the default where no separate language file exists. + if (this.options.language && this.options.language !== "en" && !this.options.language.startsWith("en")) { + let lang = this.options.language.split("-"); + // Fix for country specific languages — only for supported combined locales + const supportedCombined = new Set(["pt-BR", "pt-PT", "ug-CN", "zh-CN", "zh-TW"]); + if(lang.length>1){ + const combined =`${lang[0]}-${lang[1].toUpperCase()}`; + lang = supportedCombined.has(combined) ? combined : lang[0]; + }else{ + lang = lang[0]; + } + await import(`select2/select2_locale_${lang}`); + } + } catch { + console.warn("Language file could not be loaded", this.options.language); + } + + this.options.formatResultCssClass = (ob) => { + if (ob.id) { + return ( + "select2-option-" + + ob.id.toLowerCase().replace(/[ \:\)\(\[\]\{\}\_\+\=\&\*\%\#]/g, "-") + ); + } + }; + + function callback(action, e) { + if (action) { + if (this.options.debug) { + console.debug("callback", action, e); + } + if (typeof action === "string") { + action = window[action]; + } + return action(e); + } else { + return action; + } + } + + this.$el.select2(this.options); + + // Select2 v3 signals changes via a jQuery `change` event, which isn't + // caught by native JavaScript event listeners. + // Let's re-trigger as native `input`- and `change`-events, so that + // those can pick it up. A native `` fires both input and - // change when its value is committed, so we mirror both. - let dispatching = false; - this.$el.on("change", () => { - // The `dispatching` guard prevents infinite recursion: jQuery's - // `change` handler also catches the native `change` event we - // dispatch here. - if (dispatching) { - return; - } - dispatching = true; - this.el.dispatchEvent(events.input_event()); - this.el.dispatchEvent(events.change_event()); - dispatching = false; - }); - - // Select2 v3 signals loss of focus via a jQuery `select2-blur` event, - // which isn't caught by native JavaScript event listeners. - // Let's re-trigger as a native `blur` event so that native listeners can - // pick it up. - this.$el.on("select2-blur", () => this.el.dispatchEvent(events.blur_event())); - - this.$el.on("select2-selected", (e) => callback(this.options.onSelected, e)); - this.$el.on("select2-selecting", (e) => callback(this.options.onSelecting, e)); - this.$el.on("select2-deselecting", (e) => - callback(this.options.onDeselecting, e) - ); - this.$el.on("select2-deselected", (e) => callback(this.options.onDeselected, e)); - this.$select2 = this.$el.parent().find(".select2-container"); - this.$el.parent().off("close.plone-modal.patterns"); - if (this.options.orderable) { - this.$select2.addClass("select2-orderable"); - } - }, - - opened() { - const isOpen = $(".select2-dropdown-open", this.$el.parent()).length === 1; - return isOpen; - }, - - async init() { - const i18n = new I18n(); - this.options.language = i18n.currentLanguage; - this.options.allowNewItems = this.options.allowNewItems - ? JSON.parse(this.options.allowNewItems) - : true; - - // TODO: Select2 respects the select fields multiple attribute. - // Currently, only when multiple is set in the pattern options - // the replacement to a hidden input field is done. - // A collection's querystring widget has the multiple attribute - // but must not be replaced with a hidden inout, otherwise robot - // tests fail. - //if (this.el.hasAttribute("multiple")) { - // this.options.multiple = true; - //} - - if (this.options.ajax || this.options.vocabularyUrl) { - if (this.options.vocabularyUrl) { - this.options.multiple = - this.options.multiple === undefined ? true : this.options.multiple; - this.options.ajax = this.options.ajax || {}; - this.options.ajax.url = this.options.vocabularyUrl; - this.options.initSelection = ($el, callback) => { - const data = []; - const value = $el.val(); - for (const val of value.split(this.options.separator)) { - if (val === "") { - // Skip empty values, e.g. from an empty input. - continue; - } - const _val = utils.removeHTML(val); - data.push({ id: _val, text: _val }); - } - // Select2 v3 expects a single object for single-select - // widgets and an array for multi-select widgets. Passing an - // array to a single select leaves `data.text` undefined and - // the pre-set value is not rendered. - callback(this.options.multiple ? data : data[0] || null); - }; - } - - let queryTerm = ""; - - const ajaxTimeout = parseInt(this.options.ajaxTimeout || 300, 10); - delete this.options.ajaxTimeout; - this.options.ajax = $.extend( - { - quietMillis: ajaxTimeout, - data: (term, page) => { - queryTerm = term; - return { - query: term, - page_limit: 10, - page: page, - }; - }, - results: (data) => { - let results = data.results; - if (this.options.vocabularyUrl) { - const dataIds = []; - for (const it of data.results) { - dataIds.push(it.id); - } - results = []; - - const haveResult = - queryTerm === "" || dataIds.includes(queryTerm); - if (this.options.allowNewItems && !haveResult) { - queryTerm = utils.removeHTML(queryTerm); - results.push({ - id: queryTerm, - text: queryTerm, - }); - } - - for (const it of data.results) { - results.push(it); - } - } - return { results: results }; - }, - }, - this.options.ajax - ); - } else if (this.options.multiple && this.$el.is("select")) { - // Multiselects are converted to input[type=hidden] for Select2 - // TODO: This should actually not be necessary. - // This is kept for backwards compatibility but should be - // re-checked and removed if possible. - this.$el.attr("multiple", true); - const vals = this.$el.val() || []; - const options = [...this.el.querySelectorAll("option")].map((it) => { - return { text: it.innerHTML, id: it.value }; - }); - - const el = document.createElement("input"); - el.type = "hidden"; - el.value = vals.join(this.options.separator); - el.className = this.el.getAttribute("class"); - el.name = this.el.name; - el.id = this.el.id; - this.el.after(el); - this.el.remove(); - this.el = el; - this.$el = $(el); - this.options.data = options; - } - this.initializeValues(); - this.initializeTags(); - await this.initializeSelect2(); - await this.initializeOrdering(); + init: async function () { + const impl = (await import("./select2--implementation")).default; + // Defaults live with the implementation; merge them under the parsed + // options (which must win), reproducing the eager pattern's options. + this.options = $.extend(true, {}, impl.defaults, this.options); + // Graft onto this single instance so the pattern registers and behaves + // exactly as before, just with the heavy body loaded on demand. + $.extend(this, impl); + return impl.init.apply(this, arguments); }, });