Skip to content

Commit 175c7bd

Browse files
authored
Merge pull request #1615 from plone/mockup-lazy-select2
Lazify pat-select2: split registration from implementation
2 parents a20b2e7 + b465752 commit 175c7bd

3 files changed

Lines changed: 332 additions & 307 deletions

File tree

src/pat/relateditems/relateditems.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Base from "@patternslib/patternslib/src/core/base";
44
import _t from "../../core/i18n-wrapper";
55
import utils from "../../core/utils";
66
import registry from "@patternslib/patternslib/src/core/registry";
7-
import Select2 from "../select2/select2";
87

98
const KEY = {
109
LEFT: 37,
@@ -501,6 +500,9 @@ export default Base.extend({
501500
},
502501

503502
async init() {
503+
// select2 methods are borrowed onto this instance; load them lazily so
504+
// the select2 body stays out of the eager patterns chunk.
505+
this._select2 = (await import("../select2/select2--implementation")).default;
504506
(await import("bootstrap")).Dropdown;
505507
import("./relateditems.scss");
506508

@@ -525,8 +527,8 @@ export default Base.extend({
525527
this.$el.wrap('<div class="pat-relateditems-container" />');
526528
this.$container = this.$el.parents(".pat-relateditems-container");
527529

528-
Select2.prototype.initializeValues.call(this);
529-
Select2.prototype.initializeTags.call(this);
530+
this._select2.initializeValues.call(this);
531+
this._select2.initializeTags.call(this);
530532

531533
this.options.formatSelection = (item) => {
532534
item = $.extend(
@@ -674,8 +676,8 @@ export default Base.extend({
674676

675677
this.options.id = (item) => item.UID;
676678

677-
await Select2.prototype.initializeSelect2.call(this);
678-
await Select2.prototype.initializeOrdering.call(this);
679+
await this._select2.initializeSelect2.call(this);
680+
await this._select2.initializeOrdering.call(this);
679681

680682
this.$toolbar = $('<div class="toolbar d-flex" />');
681683
this.$container.prepend(this.$toolbar);
@@ -697,7 +699,7 @@ export default Base.extend({
697699
});
698700

699701
$(document).on("keyup", this.$el, (event) => {
700-
const isOpen = Select2.prototype.opened.call(this);
702+
const isOpen = this._select2.opened.call(this);
701703
if (!isOpen) {
702704
return;
703705
}
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import $ from "jquery";
2+
import events from "@patternslib/patternslib/src/core/events";
3+
import I18n from "../../core/i18n";
4+
import utils from "../../core/utils";
5+
6+
// Pattern config object — grafted onto the thin registered pattern in
7+
// select2.js and run there. The select2 library, its CSS and the
8+
// select2_locale_* context all load from here, keeping them (and this
9+
// wrapper) out of the eager patterns chunk. pat-relateditems borrows
10+
// these methods via a lazy import of this module.
11+
export default {
12+
defaults: {
13+
separator: ",",
14+
ajaxTimeout: 300,
15+
},
16+
17+
initializeValues() {
18+
// Init Selection ---------------------------------------------
19+
if (this.options.initialValues) {
20+
this.options.id = (term) => {
21+
return term.id;
22+
};
23+
this.options.initSelection = ($el, callback) => {
24+
const data = [];
25+
const value = $el.val();
26+
let seldefaults = this.options.initialValues;
27+
28+
// Create the initSelection value that contains the default selection,
29+
// but in a javascript object
30+
if (
31+
typeof this.options.initialValues === "string" &&
32+
this.options.initialValues !== ""
33+
) {
34+
// if default selection value starts with a '{', then treat the value as
35+
// a JSON object that needs to be parsed
36+
if (this.options.initialValues[0] === "{") {
37+
seldefaults = JSON.parse(this.options.initialValues);
38+
}
39+
// otherwise, treat the value as a list, separated by the defaults.separator value of
40+
// strings in the format "id:text", and convert it to an object
41+
else {
42+
seldefaults = {};
43+
const initial_values = $(
44+
this.options.initialValues.split(this.options.separator)
45+
);
46+
for (const it of initial_values) {
47+
const selection = it.split(":");
48+
const id = selection[0].trim();
49+
const text = selection[1].trim();
50+
seldefaults[id] = text;
51+
}
52+
}
53+
}
54+
55+
const items = $(value.split(this.options.separator));
56+
for (const it of items) {
57+
let text = it;
58+
if (seldefaults[it]) {
59+
text = seldefaults[it];
60+
}
61+
data.push({
62+
id: utils.removeHTML(it),
63+
text: utils.removeHTML(text),
64+
});
65+
}
66+
callback(data);
67+
};
68+
}
69+
},
70+
71+
initializeTags() {
72+
if (this.options.tags && typeof this.options.tags === "string") {
73+
if (this.options.tags.substr(0, 1) === "[") {
74+
this.options.tags = JSON.parse(this.options.tags);
75+
} else {
76+
this.options.tags = this.options.tags.split(this.options.separator);
77+
}
78+
}
79+
80+
if (this.options.tags && !this.options.allowNewItems) {
81+
this.options.data = this.options.tags.map((value) => {
82+
return { id: value, text: value };
83+
});
84+
this.options.multiple = true;
85+
delete this.options.tags;
86+
}
87+
},
88+
89+
async initializeOrdering() {
90+
if (!this.options.orderable) {
91+
return;
92+
}
93+
const Sortable = (await import("sortablejs")).default;
94+
const _initializeOrdering = () => {
95+
const sortable_el = this.$select2[0].querySelector(".select2-choices");
96+
new Sortable(sortable_el, {
97+
draggable: "li",
98+
dragClass: "select2-choice-dragging",
99+
chosenClass: "dragging",
100+
onStart: () => this.$el.select2("onSortStart"),
101+
onEnd: () => this.$el.select2("onSortEnd"),
102+
});
103+
};
104+
this.$el.on("change", _initializeOrdering.bind(this));
105+
_initializeOrdering();
106+
},
107+
108+
async initializeSelect2() {
109+
import("select2/select2.css");
110+
import("./select2.scss");
111+
await import("select2");
112+
try {
113+
// Don't load "en" which is the default where no separate language file exists.
114+
if (this.options.language && this.options.language !== "en" && !this.options.language.startsWith("en")) {
115+
let lang = this.options.language.split("-");
116+
// Fix for country specific languages — only for supported combined locales
117+
const supportedCombined = new Set(["pt-BR", "pt-PT", "ug-CN", "zh-CN", "zh-TW"]);
118+
if(lang.length>1){
119+
const combined =`${lang[0]}-${lang[1].toUpperCase()}`;
120+
lang = supportedCombined.has(combined) ? combined : lang[0];
121+
}else{
122+
lang = lang[0];
123+
}
124+
await import(`select2/select2_locale_${lang}`);
125+
}
126+
} catch {
127+
console.warn("Language file could not be loaded", this.options.language);
128+
}
129+
130+
this.options.formatResultCssClass = (ob) => {
131+
if (ob.id) {
132+
return (
133+
"select2-option-" +
134+
ob.id.toLowerCase().replace(/[ \:\)\(\[\]\{\}\_\+\=\&\*\%\#]/g, "-")
135+
);
136+
}
137+
};
138+
139+
function callback(action, e) {
140+
if (action) {
141+
if (this.options.debug) {
142+
console.debug("callback", action, e);
143+
}
144+
if (typeof action === "string") {
145+
action = window[action];
146+
}
147+
return action(e);
148+
} else {
149+
return action;
150+
}
151+
}
152+
153+
this.$el.select2(this.options);
154+
155+
// Select2 v3 signals changes via a jQuery `change` event, which isn't
156+
// caught by native JavaScript event listeners.
157+
// Let's re-trigger as native `input`- and `change`-events, so that
158+
// those can pick it up. A native `<select>` fires both input and
159+
// change when its value is committed, so we mirror both.
160+
let dispatching = false;
161+
this.$el.on("change", () => {
162+
// The `dispatching` guard prevents infinite recursion: jQuery's
163+
// `change` handler also catches the native `change` event we
164+
// dispatch here.
165+
if (dispatching) {
166+
return;
167+
}
168+
dispatching = true;
169+
this.el.dispatchEvent(events.input_event());
170+
this.el.dispatchEvent(events.change_event());
171+
dispatching = false;
172+
});
173+
174+
// Select2 v3 signals loss of focus via a jQuery `select2-blur` event,
175+
// which isn't caught by native JavaScript event listeners.
176+
// Let's re-trigger as a native `blur` event so that native listeners can
177+
// pick it up.
178+
this.$el.on("select2-blur", () => this.el.dispatchEvent(events.blur_event()));
179+
180+
this.$el.on("select2-selected", (e) => callback(this.options.onSelected, e));
181+
this.$el.on("select2-selecting", (e) => callback(this.options.onSelecting, e));
182+
this.$el.on("select2-deselecting", (e) =>
183+
callback(this.options.onDeselecting, e)
184+
);
185+
this.$el.on("select2-deselected", (e) => callback(this.options.onDeselected, e));
186+
this.$select2 = this.$el.parent().find(".select2-container");
187+
this.$el.parent().off("close.plone-modal.patterns");
188+
if (this.options.orderable) {
189+
this.$select2.addClass("select2-orderable");
190+
}
191+
},
192+
193+
opened() {
194+
const isOpen = $(".select2-dropdown-open", this.$el.parent()).length === 1;
195+
return isOpen;
196+
},
197+
198+
async init() {
199+
const i18n = new I18n();
200+
this.options.language = i18n.currentLanguage;
201+
this.options.allowNewItems = this.options.allowNewItems
202+
? JSON.parse(this.options.allowNewItems)
203+
: true;
204+
205+
// TODO: Select2 respects the select fields multiple attribute.
206+
// Currently, only when multiple is set in the pattern options
207+
// the replacement to a hidden input field is done.
208+
// A collection's querystring widget has the multiple attribute
209+
// but must not be replaced with a hidden inout, otherwise robot
210+
// tests fail.
211+
//if (this.el.hasAttribute("multiple")) {
212+
// this.options.multiple = true;
213+
//}
214+
215+
if (this.options.ajax || this.options.vocabularyUrl) {
216+
if (this.options.vocabularyUrl) {
217+
this.options.multiple =
218+
this.options.multiple === undefined ? true : this.options.multiple;
219+
this.options.ajax = this.options.ajax || {};
220+
this.options.ajax.url = this.options.vocabularyUrl;
221+
this.options.initSelection = ($el, callback) => {
222+
const data = [];
223+
const value = $el.val();
224+
for (const val of value.split(this.options.separator)) {
225+
if (val === "") {
226+
// Skip empty values, e.g. from an empty input.
227+
continue;
228+
}
229+
const _val = utils.removeHTML(val);
230+
data.push({ id: _val, text: _val });
231+
}
232+
// Select2 v3 expects a single object for single-select
233+
// widgets and an array for multi-select widgets. Passing an
234+
// array to a single select leaves `data.text` undefined and
235+
// the pre-set value is not rendered.
236+
callback(this.options.multiple ? data : data[0] || null);
237+
};
238+
}
239+
240+
let queryTerm = "";
241+
242+
const ajaxTimeout = parseInt(this.options.ajaxTimeout || 300, 10);
243+
delete this.options.ajaxTimeout;
244+
this.options.ajax = $.extend(
245+
{
246+
quietMillis: ajaxTimeout,
247+
data: (term, page) => {
248+
queryTerm = term;
249+
return {
250+
query: term,
251+
page_limit: 10,
252+
page: page,
253+
};
254+
},
255+
results: (data) => {
256+
let results = data.results;
257+
if (this.options.vocabularyUrl) {
258+
const dataIds = [];
259+
for (const it of data.results) {
260+
dataIds.push(it.id);
261+
}
262+
results = [];
263+
264+
const haveResult =
265+
queryTerm === "" || dataIds.includes(queryTerm);
266+
if (this.options.allowNewItems && !haveResult) {
267+
queryTerm = utils.removeHTML(queryTerm);
268+
results.push({
269+
id: queryTerm,
270+
text: queryTerm,
271+
});
272+
}
273+
274+
for (const it of data.results) {
275+
results.push(it);
276+
}
277+
}
278+
return { results: results };
279+
},
280+
},
281+
this.options.ajax
282+
);
283+
} else if (this.options.multiple && this.$el.is("select")) {
284+
// Multiselects are converted to input[type=hidden] for Select2
285+
// TODO: This should actually not be necessary.
286+
// This is kept for backwards compatibility but should be
287+
// re-checked and removed if possible.
288+
this.$el.attr("multiple", true);
289+
const vals = this.$el.val() || [];
290+
const options = [...this.el.querySelectorAll("option")].map((it) => {
291+
return { text: it.innerHTML, id: it.value };
292+
});
293+
294+
const el = document.createElement("input");
295+
el.type = "hidden";
296+
el.value = vals.join(this.options.separator);
297+
el.className = this.el.getAttribute("class");
298+
el.name = this.el.name;
299+
el.id = this.el.id;
300+
this.el.after(el);
301+
this.el.remove();
302+
this.el = el;
303+
this.$el = $(el);
304+
305+
this.options.data = options;
306+
}
307+
this.initializeValues();
308+
this.initializeTags();
309+
await this.initializeSelect2();
310+
await this.initializeOrdering();
311+
},
312+
};

0 commit comments

Comments
 (0)