imio.smartweb.common 1.2.42__py3-none-any.whl → 1.2.43__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. imio/smartweb/common/browser/tiny/process.py +1 -1
  2. imio/smartweb/common/configure.zcml +1 -0
  3. imio/smartweb/common/ia/__init__.py +0 -0
  4. imio/smartweb/common/ia/behaviors/__init__.py +0 -0
  5. imio/smartweb/common/ia/behaviors/configure.zcml +15 -0
  6. imio/smartweb/common/ia/behaviors/title.py +17 -0
  7. imio/smartweb/common/ia/browser/__init__.py +0 -0
  8. imio/smartweb/common/ia/browser/categorization_button_add.py +63 -0
  9. imio/smartweb/common/ia/browser/categorization_button_edit.py +87 -0
  10. imio/smartweb/common/ia/browser/configure.zcml +43 -0
  11. imio/smartweb/common/ia/browser/static/Makefile +12 -0
  12. imio/smartweb/common/ia/browser/static/package.json +28 -0
  13. imio/smartweb/common/ia/browser/static/smartweb-common-ia_suggested_titles-compiled.js +1 -0
  14. imio/smartweb/common/ia/browser/static/src/ia_suggested_titles.js +247 -0
  15. imio/smartweb/common/ia/browser/static/webpack.config.js +41 -0
  16. imio/smartweb/common/ia/browser/views.py +150 -0
  17. imio/smartweb/common/ia/configure.zcml +10 -0
  18. imio/smartweb/common/ia/widgets/configure.zcml +17 -0
  19. imio/smartweb/common/ia/widgets/html_snippet_widget.pt +352 -0
  20. imio/smartweb/common/ia/widgets/html_snippet_widget.py +65 -0
  21. imio/smartweb/common/ia/widgets/suggested_ia_titles_input.pt +45 -0
  22. imio/smartweb/common/ia/widgets/widget.py +40 -0
  23. imio/smartweb/common/profiles/default/metadata.xml +1 -1
  24. imio/smartweb/common/profiles/default/registry/registry.xml +9 -0
  25. imio/smartweb/common/rest/endpoint.py +10 -1
  26. imio/smartweb/common/upgrades/configure.zcml +19 -0
  27. imio/smartweb/common/upgrades/profiles/1035_to_1036/registry/bundles.xml +17 -0
  28. imio/smartweb/common/utils.py +4 -2
  29. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/METADATA +12 -1
  30. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/RECORD +36 -16
  31. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/WHEEL +1 -1
  32. imio/smartweb/common/browser/ia.py +0 -33
  33. /imio.smartweb.common-1.2.42-py3.12-nspkg.pth → /imio.smartweb.common-1.2.43-py3.12-nspkg.pth +0 -0
  34. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/licenses/LICENSE.GPL +0 -0
  35. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/licenses/LICENSE.rst +0 -0
  36. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/namespace_packages.txt +0 -0
  37. {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.43.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # src/your.pkg/browser/process.py
2
- from imio.smartweb.common.browser.ia import BaseIAView
2
+ from imio.smartweb.common.ia.browser.views import BaseIAView
3
3
  from imio.smartweb.common.config import IPA_URL
4
4
 
5
5
  import json
@@ -25,6 +25,7 @@
25
25
  <include package=".behaviors" />
26
26
  <include package=".browser" />
27
27
  <include package=".faceted" />
28
+ <include package=".ia" />
28
29
  <include package=".rest" />
29
30
  <includeOverrides file="utility_overrides.zcml" />
30
31
  <include package=".serializers" />
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:plone="http://namespaces.plone.org/plone"
4
+ i18n_domain="imio.smartweb">
5
+
6
+ <include package="plone.behavior" file="meta.zcml"/>
7
+
8
+ <plone:behavior
9
+ name="imio.smartweb.ia.titles"
10
+ title="Title"
11
+ description="Add suggested IA titles."
12
+ provides=".title.IIASmartTitle"
13
+ />
14
+
15
+ </configure>
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from imio.smartweb.common.ia.widgets.widget import SuggestedIATitlesFieldWidget
4
+ from imio.smartweb.locales import SmartwebMessageFactory as _
5
+ from plone.app.dexterity.behaviors.metadata import IBasic
6
+ from plone.autoform import directives
7
+ from plone.autoform.interfaces import IFormFieldProvider
8
+ from zope import schema
9
+ from zope.interface import provider
10
+
11
+
12
+ @provider(IFormFieldProvider)
13
+ class IIASmartTitle(IBasic):
14
+ """Behavior qui remplace le titre standard par un titre avec IA"""
15
+
16
+ directives.widget(title=SuggestedIATitlesFieldWidget)
17
+ title = schema.TextLine(title=_("Title"), required=True)
File without changes
@@ -0,0 +1,63 @@
1
+ from imio.smartweb.common.browser.forms import CustomAddForm
2
+ from imio.smartweb.common.ia.widgets.html_snippet_widget import AddHtmlSnippetWidget
3
+ from plone.dexterity.browser.add import DefaultAddView
4
+ from z3c.form.interfaces import HIDDEN_MODE, DISPLAY_MODE
5
+ from z3c.form.widget import FieldWidget
6
+ from zope import schema
7
+
8
+ FIELD_NAME = "categorization_ia_link"
9
+
10
+
11
+ class IACategorizeAddForm(CustomAddForm):
12
+
13
+ def update(self):
14
+ super(IACategorizeAddForm, self).update()
15
+
16
+ for group in self.groups:
17
+ if getattr(group, "__name__", "") == "layout":
18
+ if "hide_title" in group.widgets:
19
+ group.widgets["hide_title"].mode = HIDDEN_MODE
20
+ group.widgets["hide_title"].value = ["selected"]
21
+
22
+ # 2) Inject HTML button in "categorization" group
23
+ if not getattr(self, "groups", None):
24
+ self.updateGroups()
25
+
26
+ cat = next(
27
+ (g for g in self.groups if getattr(g, "__name__", "") == "categorization"),
28
+ None,
29
+ )
30
+ if not cat or not getattr(cat, "widgets", None):
31
+ return
32
+
33
+ # Avoid doublons (refresh)
34
+ for k in getattr(cat.widgets, "keys", lambda: [])():
35
+ if k.endswith(FIELD_NAME) or k == FIELD_NAME:
36
+ return
37
+
38
+ # Create dummy field + FieldWidget(HtmlSnippetWidget)
39
+ zfield = schema.Text(__name__=FIELD_NAME, title="", description="")
40
+ w = FieldWidget(zfield, AddHtmlSnippetWidget(self.request))
41
+ w.mode = DISPLAY_MODE
42
+ w.context = self.context # conteneur du futur objet
43
+ w.form = self
44
+ w.ignoreContext = True
45
+ w.label = ""
46
+ w.update()
47
+
48
+ try:
49
+ cat.widgets[f"form.widgets.{FIELD_NAME}"] = w
50
+ except Exception:
51
+ try:
52
+ cat.widgets[FIELD_NAME] = w
53
+ except Exception:
54
+ # Fallback bas niveau si nécessaire
55
+ if hasattr(cat.widgets, "_data_keys") and hasattr(
56
+ cat.widgets, "_widgets"
57
+ ):
58
+ cat.widgets._data_keys.append(FIELD_NAME)
59
+ cat.widgets._widgets.append(w)
60
+
61
+
62
+ class IACategorizeAddView(DefaultAddView):
63
+ form = IACategorizeAddForm
@@ -0,0 +1,87 @@
1
+ # -*- coding: utf-8 -*-
2
+ from imio.smartweb.common.browser.forms import CustomEditForm
3
+ from imio.smartweb.common.ia.widgets.html_snippet_widget import EditHtmlSnippetWidget
4
+ from plone.z3cform import layout
5
+ from zope import schema
6
+ from z3c.form.interfaces import DISPLAY_MODE
7
+ from z3c.form.widget import FieldWidget
8
+
9
+
10
+ FIELD_NAME = "categorization_ia_link" # Internal id for dummy field
11
+
12
+
13
+ class IACategorizeEditForm(CustomEditForm):
14
+ """Vue edit custom, avec bouton 'Catégoriser' injecté en haut de 'categorization'."""
15
+
16
+ def update(self):
17
+ super(IACategorizeEditForm, self).update()
18
+
19
+ # Inject button on top of 'categorization' fieldset
20
+ if not getattr(self, "groups", None):
21
+ return
22
+
23
+ # Find 'categorization' group
24
+ cat = next(
25
+ (g for g in self.groups if getattr(g, "__name__", "") == "categorization"),
26
+ None,
27
+ )
28
+ if not cat or not getattr(cat, "widgets", None):
29
+ return
30
+
31
+ # Avoid doublons (refresh) if already here => stop
32
+ existing_keys = list(getattr(cat.widgets, "keys", lambda: [])())
33
+ for k in existing_keys:
34
+ if (
35
+ k.endswith(FIELD_NAME)
36
+ or k == FIELD_NAME
37
+ or k.endswith(f"form.widgets.{FIELD_NAME}")
38
+ ):
39
+ return
40
+
41
+ # Create dummy field + FieldWidget(HtmlSnippetWidget)
42
+ zfield = schema.Text(__name__=FIELD_NAME, title="", description="")
43
+ w = FieldWidget(zfield, EditHtmlSnippetWidget(self.request))
44
+ w.mode = DISPLAY_MODE
45
+ w.context = self.context
46
+ w.form = self
47
+ w.ignoreContext = True
48
+ w.label = ""
49
+ w.update() # prépare endpoint/wid
50
+
51
+ key = f"form.widgets.{FIELD_NAME}"
52
+
53
+ if hasattr(cat.widgets, "_data_keys") and hasattr(cat.widgets, "_widgets"):
54
+ # Remove residual occurrences
55
+ try:
56
+ while key in cat.widgets._data_keys:
57
+ idx = cat.widgets._data_keys.index(key)
58
+ cat.widgets._data_keys.pop(idx)
59
+ cat.widgets._widgets.pop(idx)
60
+ except Exception:
61
+ pass
62
+ cat.widgets._data_keys.insert(0, key)
63
+ cat.widgets._widgets.insert(0, w)
64
+ return
65
+
66
+ # fallback : rebuild mapping with our with our widget
67
+ try:
68
+ existing = [(k, cat.widgets[k]) for k in list(cat.widgets.keys())]
69
+ try:
70
+ cat.widgets.clear()
71
+ except Exception:
72
+ pass
73
+ # Firstly, our widget
74
+ cat.widgets[key] = w
75
+ # Next...
76
+ for k, ww in existing:
77
+ if k != key:
78
+ cat.widgets[k] = ww
79
+ except Exception:
80
+ # Last resort: If nothing else worked
81
+ try:
82
+ cat.widgets[key] = w
83
+ except Exception:
84
+ pass
85
+
86
+
87
+ IACategorizeEditView = layout.wrap_form(IACategorizeEditForm)
@@ -0,0 +1,43 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:browser="http://namespaces.zope.org/browser"
4
+ xmlns:plone="http://namespaces.plone.org/plone"
5
+ xmlns:z3c="http://namespaces.zope.org/z3c"
6
+ >
7
+
8
+ <plone:static
9
+ directory="static"
10
+ name="imio.smartweb.common.ia"
11
+ type="plone"
12
+ />
13
+
14
+ <browser:page
15
+ name="processsuggestedtitles"
16
+ for="*"
17
+ class=".views.ProcessSuggestedTitlesView"
18
+ permission="cmf.ModifyPortalContent"
19
+ />
20
+
21
+ <browser:page
22
+ name="ProcessCategorizeContent"
23
+ for="*"
24
+ class=".views.BaseProcessCategorizeContentView"
25
+ permission="cmf.ModifyPortalContent"
26
+ />
27
+
28
+ <browser:page
29
+ for="plone.dexterity.interfaces.IDexterityContainer"
30
+ name="edit"
31
+ class=".categorization_button_edit.IACategorizeEditForm"
32
+ permission="cmf.ModifyPortalContent"
33
+ layer="imio.smartweb.common.interfaces.IImioSmartwebCommonLayer"
34
+ />
35
+
36
+ <class class=".categorization_button_add.IACategorizeAddView">
37
+ <require
38
+ permission="cmf.AddPortalContent"
39
+ interface="zope.publisher.interfaces.browser.IBrowserPage"
40
+ />
41
+ </class>
42
+
43
+ </configure>
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/make
2
+
3
+ help:
4
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
5
+
6
+ .PHONY: install
7
+ install: ## Install webcomponents dependencies
8
+ npm install
9
+
10
+ .PHONY: build
11
+ build: ## Build the production bundle
12
+ npm run build
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "imio.smartweb.common",
3
+ "version": "1.0.0",
4
+ "description": "imio smartweb common ia static",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/IMIO/imio.smartweb.common"
8
+ },
9
+ "author": "iMio",
10
+ "license": "GPLv2",
11
+ "dependencies": {
12
+ "webpack": "^5.75.0"
13
+ },
14
+ "scripts": {
15
+ "build": "webpack --mode=production --progress --stats"
16
+ },
17
+ "devDependencies": {
18
+ "babel-loader": "^8.2.2",
19
+ "css-loader": "^6.7.2",
20
+ "css-minimizer-webpack-plugin": "^3.4.1",
21
+ "less": "^4.1.3",
22
+ "less-loader": "^11.1.0",
23
+ "mini-css-extract-plugin": "^2.7.2",
24
+ "style-loader": "^3.3.1",
25
+ "terser-webpack-plugin": "^5.3.6",
26
+ "webpack-cli": "^5.0.1"
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ (()=>{"use strict";let t=null,e=null;function o(t,e=24){const n=document.querySelectorAll(".modal.show .ia-suggest-list, .plone-modal .ia-suggest-list, .plone-modal-container .ia-suggest-list");n.length?n.forEach(e=>e.innerHTML=t):e>0&&requestAnimationFrame(()=>o(t,e-1))}function n(t){if(!t)return!1;const e=t.getClientRects?.();return null!==t.offsetParent||e&&e.length>0}function r(o,r){const i=e||t?.dataset.sourceSelector||"#form-widgets-title",s=document.querySelector(i)||t?.querySelector('input[type="text"]');s&&(s.value=o,s.dispatchEvent(new Event("input",{bubbles:!0})),s.dispatchEvent(new Event("change",{bubbles:!0})),s.focus()),function(t){const e=t?.closest(".modal")||document.querySelector(".modal.show")||document.querySelector(".modal");if(!e)return;const o=e.closest(".modal-wrapper");let r=e.querySelector(".modal-close, .plone-modal-close")||e.querySelector('[data-bs-dismiss="modal"], [data-dismiss="modal"]');r&&r.click();try{window.bootstrap?.Modal&&(window.bootstrap.Modal.getInstance(e)||new window.bootstrap.Modal(e)).hide()}catch{}setTimeout(()=>{(e.classList.contains("show")||n(e))&&document.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape",bubbles:!0}))},50),setTimeout(()=>{(e.classList.contains("show")||n(e))&&(e.classList.remove("show"),e.style.display="none",e.setAttribute("aria-hidden","true")),o&&o.parentNode&&o.remove(),(o&&o.parentNode||document.body).querySelectorAll(".modal-backdrop, .plone-modal-backdrop, .plone-modal-overlay, .backdrop, .backdrop-active").forEach(t=>t.remove()),document.body.classList.remove("modal-open"),document.body.style.removeProperty("padding-right"),document.querySelectorAll("[inert]").forEach(t=>t.removeAttribute("inert"))},150)}(r||document.body)}function i(t){const e=t.target.closest(".ia-suggest-item");if(!e)return;"click"!==t.type&&"pointerup"!==t.type||t.stopPropagation();const o=e.getAttribute("data-title")||e.textContent.trim();o&&r(o,e)}document.addEventListener("click",n=>{const r=n.target.closest("a.suggestedtitles-generate-button.pat-plone-modal");if(!r)return;t=r.closest(".suggestedtitles-with-button")||null,e=t?.dataset.sourceSelector||"#form-widgets-title";const i=t?.dataset.slugUrl;if(!i)return;const s=r.getAttribute("href"),a=s?document.querySelector(s):null,c=a?a.querySelector(".ia-suggest-list"):null;c&&(c.textContent="Loading…"),async function(t,e){const o={Accept:"application/json"},n=document.querySelector('input[name="_authenticator"]')?.value||"",r=new URLSearchParams({text:e});try{const e=await fetch(t,{method:"POST",credentials:"same-origin",headers:{...o,"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",...n?{"X-CSRF-TOKEN":n}:{}},body:r.toString()});if(!e.ok)throw new Error(`HTTP ${e.status}`);return await e.json()}catch(n){const r=t+(t.includes("?")?"&":"?")+"text="+encodeURIComponent(e),i=await fetch(r,{credentials:"same-origin",headers:o});if(!i.ok)throw n;return await i.json()}}(i,function(){try{if(window.tinymce){const t=tinymce.get("form-widgets-IRichTextBehavior-text")||tinymce.editors&&tinymce.editors.find(t=>t?.id?.includes("IRichTextBehavior"))||tinymce.activeEditor,e=t?.getContent?.();if(e)return e}}catch{}const t=document.querySelector("#form-widgets-IRichTextBehavior-text")||document.querySelector('textarea[name="form.widgets.IRichTextBehavior.text"]');return t?.value||""}()).then(t=>{const e=function(t){if("string"==typeof t)try{t=JSON.parse(t)}catch{return[t.trim()].filter(Boolean)}if(Array.isArray(t))return Array.from(new Set(t.map(t=>String(t).trim()).filter(Boolean)));if(t&&"object"==typeof t){let e=[];if(Array.isArray(t.suggested_titles))e=t.suggested_titles;else if(Array.isArray(t.titles))e=t.titles;else{const o=Object.values(t).find(t=>Array.isArray(t));o&&(e=o)}return Array.from(new Set(e.map(t=>String(t).trim()).filter(Boolean)))}return[]}(t),n=function(t){if(!t.length)return"<p>No suggestion.</p>";const e=document.createElement("ul");e.className="list-group ia-suggest-ul",e.setAttribute("role","listbox"),e.setAttribute("aria-label","Title suggestions");for(const o of t){const t=document.createElement("li");t.className="list-group-item list-group-item-action ia-suggest-item",t.setAttribute("role","option"),t.setAttribute("tabindex","0"),t.setAttribute("data-title",o),t.textContent=o,e.appendChild(t)}const o=document.createElement("div");return o.appendChild(e),o.innerHTML}(e);c&&(c.innerHTML=n),o(n)}).catch(t=>{const e='<div class="alert alert-warning">Not possible to get suggestions.</div>';c&&(c.innerHTML=e),o(e)})},!0),document.addEventListener("click",i,!0),document.addEventListener("pointerup",i,!0),document.addEventListener("keydown",function(t){if("Enter"!==t.key&&" "!==t.key)return;const e=t.target.closest(".ia-suggest-item");if(!e)return;t.preventDefault(),t.stopPropagation();const o=e.getAttribute("data-title")||e.textContent.trim();o&&r(o,e)},!0),document.addEventListener("hidden.bs.modal",()=>{t=null,e=null},!0)})();
@@ -0,0 +1,247 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ let activeContainer = null;
5
+ let activeSourceSelector = null;
6
+
7
+ // ---------- Utils ---------------------------------------------------------
8
+
9
+ function getRichTextHtml() {
10
+ try {
11
+ if (window.tinymce) {
12
+ const ed =
13
+ tinymce.get('form-widgets-IRichTextBehavior-text') ||
14
+ (tinymce.editors && tinymce.editors.find(e => e?.id?.includes('IRichTextBehavior'))) ||
15
+ tinymce.activeEditor;
16
+ const html = ed?.getContent?.();
17
+ if (html) return html;
18
+ }
19
+ } catch {}
20
+ const ta =
21
+ document.querySelector('#form-widgets-IRichTextBehavior-text') ||
22
+ document.querySelector('textarea[name="form.widgets.IRichTextBehavior.text"]');
23
+ return ta?.value || '';
24
+ }
25
+
26
+ function normalizeTitles(data) {
27
+ if (typeof data === 'string') {
28
+ try { data = JSON.parse(data); } catch { return [data.trim()].filter(Boolean); }
29
+ }
30
+ if (Array.isArray(data)) {
31
+ return Array.from(new Set(data.map(t => String(t).trim()).filter(Boolean)));
32
+ }
33
+ if (data && typeof data === 'object') {
34
+ let arr = [];
35
+ if (Array.isArray(data.suggested_titles)) {
36
+ arr = data.suggested_titles;
37
+ } else if (Array.isArray(data.titles)) {
38
+ arr = data.titles;
39
+ } else {
40
+ const firstArray = Object.values(data).find(v => Array.isArray(v));
41
+ if (firstArray) arr = firstArray;
42
+ }
43
+ return Array.from(new Set(arr.map(t => String(t).trim()).filter(Boolean)));
44
+ }
45
+ return [];
46
+ }
47
+
48
+ function buildListHTML(titles) {
49
+ if (!titles.length) return '<p>No suggestion.</p>';
50
+
51
+ const ul = document.createElement('ul');
52
+ ul.className = 'list-group ia-suggest-ul';
53
+ ul.setAttribute('role', 'listbox');
54
+ ul.setAttribute('aria-label', 'Title suggestions');
55
+
56
+ for (const t of titles) {
57
+ const li = document.createElement('li');
58
+ li.className = 'list-group-item list-group-item-action ia-suggest-item';
59
+ li.setAttribute('role', 'option');
60
+ li.setAttribute('tabindex', '0');
61
+ li.setAttribute('data-title', t);
62
+ li.textContent = t;
63
+ ul.appendChild(li);
64
+ }
65
+
66
+ const wrap = document.createElement('div');
67
+ wrap.appendChild(ul);
68
+ return wrap.innerHTML;
69
+ }
70
+
71
+ function mirrorIntoOpenModal(listHTML, tries = 24) {
72
+ const lists = document.querySelectorAll(
73
+ '.modal.show .ia-suggest-list, .plone-modal .ia-suggest-list, .plone-modal-container .ia-suggest-list'
74
+ );
75
+ if (lists.length) {
76
+ lists.forEach(el => (el.innerHTML = listHTML));
77
+ return;
78
+ }
79
+ if (tries > 0) requestAnimationFrame(() => mirrorIntoOpenModal(listHTML, tries - 1));
80
+ }
81
+
82
+ async function fetchSuggestions(slugUrl, textHtml) {
83
+ const headers = { Accept: 'application/json' };
84
+ const token = document.querySelector('input[name="_authenticator"]')?.value || '';
85
+ const body = new URLSearchParams({ text: textHtml });
86
+
87
+ try {
88
+ const res = await fetch(slugUrl, {
89
+ method: 'POST',
90
+ credentials: 'same-origin',
91
+ headers: {
92
+ ...headers,
93
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
94
+ ...(token ? { 'X-CSRF-TOKEN': token } : {}),
95
+ },
96
+ body: body.toString(),
97
+ });
98
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
99
+ return await res.json();
100
+ } catch (e) {
101
+ const url = slugUrl + (slugUrl.includes('?') ? '&' : '?') + 'text=' + encodeURIComponent(textHtml);
102
+ const res = await fetch(url, { credentials: 'same-origin', headers });
103
+ if (!res.ok) throw e;
104
+ return await res.json();
105
+ }
106
+ }
107
+
108
+ // ---------- Fermeture locale ciblant .modal et .modal-wrapper --------------
109
+
110
+ function isVisible(el) {
111
+ if (!el) return false;
112
+ const rects = el.getClientRects?.();
113
+ return (el.offsetParent !== null) || (rects && rects.length > 0);
114
+ }
115
+
116
+ function closeModal(fromEl) {
117
+ // Modale et wrapper les plus proches de l'élément qui a été cliqué
118
+ const modal =
119
+ fromEl?.closest('.modal') ||
120
+ document.querySelector('.modal.show') ||
121
+ document.querySelector('.modal');
122
+
123
+ if (!modal) return;
124
+ const wrapper = modal.closest('.modal-wrapper');
125
+
126
+ // 1) Fermer via les contrôles internes
127
+ let closer =
128
+ modal.querySelector('.modal-close, .plone-modal-close') ||
129
+ modal.querySelector('[data-bs-dismiss="modal"], [data-dismiss="modal"]');
130
+
131
+ if (closer) closer.click();
132
+
133
+ // 2) API Bootstrap si dispo
134
+ try {
135
+ if (window.bootstrap?.Modal) {
136
+ const inst = window.bootstrap.Modal.getInstance(modal) || new window.bootstrap.Modal(modal);
137
+ inst.hide();
138
+ }
139
+ } catch {}
140
+
141
+ // 3) Coup d'ESC pour aider certains thèmes
142
+ setTimeout(() => {
143
+ if (modal.classList.contains('show') || isVisible(modal)) {
144
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
145
+ }
146
+ }, 50);
147
+
148
+ // 4) Si encore visible → masquer juste cette modale + retirer son wrapper
149
+ setTimeout(() => {
150
+ if (modal.classList.contains('show') || isVisible(modal)) {
151
+ modal.classList.remove('show');
152
+ modal.style.display = 'none';
153
+ modal.setAttribute('aria-hidden', 'true');
154
+ }
155
+ if (wrapper && wrapper.parentNode) {
156
+ wrapper.remove(); // <- ton cas : .modal-wrapper restait en place
157
+ }
158
+ // Nettoyage léger (local)
159
+ const parent = (wrapper && wrapper.parentNode) || document.body;
160
+ parent.querySelectorAll('.modal-backdrop, .plone-modal-backdrop, .plone-modal-overlay, .backdrop, .backdrop-active')
161
+ .forEach(n => n.remove());
162
+ document.body.classList.remove('modal-open');
163
+ document.body.style.removeProperty('padding-right');
164
+ document.querySelectorAll('[inert]').forEach(n => n.removeAttribute('inert'));
165
+ }, 150);
166
+ }
167
+
168
+ function fillInputAndClose(title, fromEl) {
169
+ const selector = activeSourceSelector || activeContainer?.dataset.sourceSelector || '#form-widgets-title';
170
+ const input = document.querySelector(selector) || activeContainer?.querySelector('input[type="text"]');
171
+ if (input) {
172
+ input.value = title;
173
+ input.dispatchEvent(new Event('input', { bubbles: true }));
174
+ input.dispatchEvent(new Event('change', { bubbles: true }));
175
+ input.focus();
176
+ }
177
+ closeModal(fromEl || document.body);
178
+ }
179
+
180
+ // ---------- 1) Trigger: mémoriser + fetch + afficher ----------------------
181
+
182
+ document.addEventListener('click', (ev) => {
183
+ const trigger = ev.target.closest('a.suggestedtitles-generate-button.pat-plone-modal');
184
+ if (!trigger) return;
185
+
186
+ activeContainer = trigger.closest('.suggestedtitles-with-button') || null;
187
+ activeSourceSelector = activeContainer?.dataset.sourceSelector || '#form-widgets-title';
188
+
189
+ const slugUrl = activeContainer?.dataset.slugUrl;
190
+ if (!slugUrl) return;
191
+
192
+ const targetSel = trigger.getAttribute('href'); // "#ia-suggest-modal"
193
+ const templateRoot = targetSel ? document.querySelector(targetSel) : null;
194
+ const templateList = templateRoot ? templateRoot.querySelector('.ia-suggest-list') : null;
195
+
196
+ if (templateList) templateList.textContent = 'Loading…';
197
+
198
+ const textHtml = getRichTextHtml();
199
+
200
+ fetchSuggestions(slugUrl, textHtml)
201
+ .then((data) => {
202
+ const titles = normalizeTitles(data);
203
+ const listHTML = buildListHTML(titles);
204
+ if (templateList) templateList.innerHTML = listHTML;
205
+ mirrorIntoOpenModal(listHTML);
206
+ })
207
+ .catch((err) => {
208
+ // (${String(err)})
209
+ const errorHTML = `<div class="alert alert-warning">Not possible to get suggestions.</div>`;
210
+ if (templateList) templateList.innerHTML = errorHTML;
211
+ mirrorIntoOpenModal(errorHTML);
212
+ });
213
+ }, true); // capture pour précéder l'ouverture pat-plone-modal
214
+
215
+ // ---------- 2) Sélection d’un titre (clic/pointer + clavier) --------------
216
+
217
+ function handlePickEvent(ev) {
218
+ const item = ev.target.closest('.ia-suggest-item');
219
+ if (!item) return;
220
+ if (ev.type === 'click' || ev.type === 'pointerup') ev.stopPropagation();
221
+ const title = item.getAttribute('data-title') || item.textContent.trim();
222
+ if (!title) return;
223
+ fillInputAndClose(title, item);
224
+ }
225
+ document.addEventListener('click', handlePickEvent, true);
226
+ document.addEventListener('pointerup', handlePickEvent, true);
227
+
228
+ function handleKey(ev) {
229
+ if (ev.key !== 'Enter' && ev.key !== ' ') return;
230
+ const item = ev.target.closest('.ia-suggest-item');
231
+ if (!item) return;
232
+ ev.preventDefault();
233
+ ev.stopPropagation();
234
+ const title = item.getAttribute('data-title') || item.textContent.trim();
235
+ if (!title) return;
236
+ fillInputAndClose(title, item);
237
+ }
238
+ document.addEventListener('keydown', handleKey, true);
239
+
240
+ // ---------- 3) Nettoyage à la fermeture (si event dispo) ------------------
241
+
242
+ document.addEventListener('hidden.bs.modal', () => {
243
+ activeContainer = null;
244
+ activeSourceSelector = null;
245
+ }, true);
246
+
247
+ })();
@@ -0,0 +1,41 @@
1
+ const path = require('path');
2
+ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3
+ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
4
+ const TerserPlugin = require("terser-webpack-plugin");
5
+
6
+
7
+ module.exports = {
8
+ mode: 'development',
9
+ entry: {
10
+ ia_suggested_titles: './src/ia_suggested_titles.js',
11
+ },
12
+ output: {
13
+ filename: 'smartweb-common-[name]-compiled.js',
14
+ path: path.resolve(__dirname, ''),
15
+ },
16
+ module: {
17
+ rules: [
18
+ {
19
+ test: /\.less$/,
20
+ use: [
21
+ MiniCssExtractPlugin.loader,
22
+ 'css-loader',
23
+ 'less-loader'
24
+ ],
25
+ },
26
+ ]
27
+ },
28
+ plugins: [
29
+ new MiniCssExtractPlugin({
30
+ filename: 'smartweb-common-[name]-compiled.css',
31
+ }),
32
+ ],
33
+ optimization: {
34
+ usedExports: true,
35
+ minimize: true,
36
+ minimizer: [
37
+ new CssMinimizerPlugin(),
38
+ new TerserPlugin()
39
+ ],
40
+ },
41
+ };