imio.smartweb.common 1.2.42__py3-none-any.whl → 1.2.44__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.
- imio/smartweb/common/browser/tiny/process.py +1 -1
- imio/smartweb/common/configure.zcml +1 -0
- imio/smartweb/common/ia/__init__.py +0 -0
- imio/smartweb/common/ia/behaviors/__init__.py +0 -0
- imio/smartweb/common/ia/behaviors/configure.zcml +15 -0
- imio/smartweb/common/ia/behaviors/title.py +17 -0
- imio/smartweb/common/ia/browser/__init__.py +0 -0
- imio/smartweb/common/ia/browser/categorization_button_add.py +63 -0
- imio/smartweb/common/ia/browser/categorization_button_edit.py +87 -0
- imio/smartweb/common/ia/browser/configure.zcml +43 -0
- imio/smartweb/common/ia/browser/static/Makefile +12 -0
- imio/smartweb/common/ia/browser/static/package.json +28 -0
- imio/smartweb/common/ia/browser/static/smartweb-common-ia_suggested_titles-compiled.js +1 -0
- imio/smartweb/common/ia/browser/static/src/ia_suggested_titles.js +247 -0
- imio/smartweb/common/ia/browser/static/webpack.config.js +41 -0
- imio/smartweb/common/ia/browser/views.py +150 -0
- imio/smartweb/common/ia/configure.zcml +10 -0
- imio/smartweb/common/ia/widgets/configure.zcml +17 -0
- imio/smartweb/common/ia/widgets/html_snippet_widget.pt +352 -0
- imio/smartweb/common/ia/widgets/html_snippet_widget.py +65 -0
- imio/smartweb/common/ia/widgets/suggested_ia_titles_input.pt +45 -0
- imio/smartweb/common/ia/widgets/widget.py +40 -0
- imio/smartweb/common/profiles/default/metadata.xml +1 -1
- imio/smartweb/common/profiles/default/registry/registry.xml +9 -0
- imio/smartweb/common/rest/endpoint.py +10 -1
- imio/smartweb/common/upgrades/configure.zcml +19 -0
- imio/smartweb/common/upgrades/profiles/1035_to_1036/registry/bundles.xml +17 -0
- imio/smartweb/common/utils.py +4 -2
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/METADATA +19 -1
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/RECORD +36 -16
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/WHEEL +1 -1
- imio/smartweb/common/browser/ia.py +0 -33
- /imio.smartweb.common-1.2.42-py3.12-nspkg.pth → /imio.smartweb.common-1.2.44-py3.12-nspkg.pth +0 -0
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/licenses/LICENSE.GPL +0 -0
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/licenses/LICENSE.rst +0 -0
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/namespace_packages.txt +0 -0
- {imio_smartweb_common-1.2.42.dist-info → imio_smartweb_common-1.2.44.dist-info}/top_level.txt +0 -0
|
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
|
+
};
|