@seliseblocks/mailcraft 0.2.5 → 0.2.7
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.
- package/DOCS.md +4 -3
- package/README.md +4 -1
- package/README.md.txt +4 -1
- package/dist/mailcraft-editor.bundle.js +57 -54
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/vanilla.html +19 -18
- package/package.json +1 -1
- package/src/core/brand.js +81 -0
- package/src/core/editor-core.js +41 -8
- package/src/core/export.js +8 -1
- package/src/core/i18n/ar.js +2 -2
- package/src/core/i18n/bg.js +2 -2
- package/src/core/i18n/bn.js +2 -2
- package/src/core/i18n/ca.js +2 -2
- package/src/core/i18n/cs.js +2 -2
- package/src/core/i18n/da.js +2 -2
- package/src/core/i18n/de-CH.js +2 -2
- package/src/core/i18n/de.js +2 -2
- package/src/core/i18n/dz.js +2 -2
- package/src/core/i18n/el.js +2 -2
- package/src/core/i18n/en.js +2 -2
- package/src/core/i18n/es.js +2 -2
- package/src/core/i18n/et.js +2 -2
- package/src/core/i18n/fi.js +2 -2
- package/src/core/i18n/fr.js +2 -2
- package/src/core/i18n/hr.js +2 -2
- package/src/core/i18n/hu.js +2 -2
- package/src/core/i18n/it.js +2 -2
- package/src/core/i18n/lt.js +2 -2
- package/src/core/i18n/lv.js +2 -2
- package/src/core/i18n/nb.js +2 -2
- package/src/core/i18n/nl.js +2 -2
- package/src/core/i18n/pl.js +2 -2
- package/src/core/i18n/pt.js +2 -2
- package/src/core/i18n/ro.js +2 -2
- package/src/core/i18n/ru.js +2 -2
- package/src/core/i18n/sk.js +2 -2
- package/src/core/i18n/sl.js +2 -2
- package/src/core/i18n/sv.js +2 -2
- package/src/core/i18n/tr.js +2 -2
- package/src/core/i18n/uk.js +2 -2
- package/src/core/icons.js +1 -0
- package/src/core/import-html.js +63 -9
- package/src/core/sanitize.js +34 -0
- package/src/core/theme.js +1 -1
- package/src/mailcraft-editor.js +20 -10
- package/src/render/block-body.js +12 -6
- package/src/render/canvas.js +7 -1
- package/src/render/fields.js +9 -1
- package/src/render/screenshot.js +13 -2
- package/src/render/style.js +14 -11
package/src/core/i18n/hu.js
CHANGED
|
@@ -66,7 +66,7 @@ export const HU = {
|
|
|
66
66
|
'Szakaszalapú: először helyezz el egy elrendezést, majd blokkokat az oszlopaiba — minden szakasznak saját háttere, kerete, flex vagy rács beállításai vannak.',
|
|
67
67
|
'rows.hintStack': 'Szabad halmozás: elrendezések nélkül — minden elhelyezett blokk saját teljes szélességű szakasszá válik, sorrendben egymásra halmozva.',
|
|
68
68
|
'files.libraryCount': 'Könyvtár — {count} fájl',
|
|
69
|
-
'files.openManager': '
|
|
69
|
+
'files.openManager': 'Médiatár megnyitása',
|
|
70
70
|
'files.dragToCanvas': 'Húzz bármely fájlt közvetlenül a rajzvászonra, hogy képblokként helyezd el.',
|
|
71
71
|
'layers.structure': 'Struktúra',
|
|
72
72
|
'layers.sectionSingle': 'Szakasz',
|
|
@@ -82,7 +82,7 @@ export const HU = {
|
|
|
82
82
|
'vars.noMatch': 'Egyetlen változó sem felel meg ennek a szűrőnek.',
|
|
83
83
|
'vars.noneDeclared': 'Az alkalmazásod még nem deklarált változókat.',
|
|
84
84
|
'vars.rteHint': 'Szöveg szerkesztése közben egy változót közvetlenül a beágyazott eszköztár legördülő menüjéből is kiválaszthatsz.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Médiatár',
|
|
86
86
|
'modal.assetsTitle': 'Erőforrások',
|
|
87
87
|
'modal.replaceImage': 'Kép cseréje',
|
|
88
88
|
'library.searchPlaceholder': 'Fájlok keresése…',
|
package/src/core/i18n/it.js
CHANGED
|
@@ -66,7 +66,7 @@ export const IT = {
|
|
|
66
66
|
'Basato su sezioni: rilascia prima un layout, poi i blocchi nelle sue colonne — ogni sezione ha proprio sfondo, bordo, impostazioni flex o griglia.',
|
|
67
67
|
'rows.hintStack': 'Pila libera: senza layout — ogni blocco rilasciato diventa una propria sezione a piena larghezza, impilata in ordine.',
|
|
68
68
|
'files.libraryCount': 'Libreria — {count} file',
|
|
69
|
-
'files.openManager': 'Apri
|
|
69
|
+
'files.openManager': 'Apri libreria multimediale',
|
|
70
70
|
'files.dragToCanvas': 'Trascina un qualsiasi file direttamente sul canvas per inserirlo come blocco immagine.',
|
|
71
71
|
'layers.structure': 'Struttura',
|
|
72
72
|
'layers.sectionSingle': 'Sezione',
|
|
@@ -82,7 +82,7 @@ export const IT = {
|
|
|
82
82
|
'vars.noMatch': 'Nessuna variabile corrisponde a questo filtro.',
|
|
83
83
|
'vars.noneDeclared': 'La tua applicazione non ha ancora dichiarato variabili.',
|
|
84
84
|
'vars.rteHint': 'Durante la modifica del testo puoi anche scegliere una variabile direttamente dal menu della barra degli strumenti in linea.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Libreria multimediale',
|
|
86
86
|
'modal.assetsTitle': 'Risorse',
|
|
87
87
|
'modal.replaceImage': 'Sostituisci immagine',
|
|
88
88
|
'library.searchPlaceholder': 'Cerca file…',
|
package/src/core/i18n/lt.js
CHANGED
|
@@ -66,7 +66,7 @@ export const LT = {
|
|
|
66
66
|
'Pagrįsta sekcijomis: pirmiausia numeskite išdėstymą, tada blokus į jo stulpelius — kiekviena sekcija turi savo foną, kraštinę, flex arba tinklelio nustatymus.',
|
|
67
67
|
'rows.hintStack': 'Laisvas krūvinys: be išdėstymų — kiekvienas numestas blokas tampa savo pilno pločio sekcija, sudėta iš eilės.',
|
|
68
68
|
'files.libraryCount': 'Biblioteka — {count} failų',
|
|
69
|
-
'files.openManager': 'Atidaryti
|
|
69
|
+
'files.openManager': 'Atidaryti medijos biblioteką',
|
|
70
70
|
'files.dragToCanvas': 'Vilkite bet kurį failą tiesiai į drobę, kad jį padėtumėte kaip vaizdo bloką.',
|
|
71
71
|
'layers.structure': 'Struktūra',
|
|
72
72
|
'layers.sectionSingle': 'Sekcija',
|
|
@@ -82,7 +82,7 @@ export const LT = {
|
|
|
82
82
|
'vars.noMatch': 'Nė vienas kintamasis neatitinka šio filtro.',
|
|
83
83
|
'vars.noneDeclared': 'Jūsų programa dar nedeklaravo jokių kintamųjų.',
|
|
84
84
|
'vars.rteHint': 'Redaguodami tekstą taip pat galite pasirinkti kintamąjį tiesiai iš įterptos įrankių juostos išskleidžiamojo meniu.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Medijos biblioteka',
|
|
86
86
|
'modal.assetsTitle': 'Ištekliai',
|
|
87
87
|
'modal.replaceImage': 'Pakeisti vaizdą',
|
|
88
88
|
'library.searchPlaceholder': 'Ieškoti failų…',
|
package/src/core/i18n/lv.js
CHANGED
|
@@ -66,7 +66,7 @@ export const LV = {
|
|
|
66
66
|
'Balstīts uz sadaļām: vispirms nometiet izkārtojumu, tad blokus tā kolonnās — katrai sadaļai ir sava fona, apmales, flex vai režģa iestatījumi.',
|
|
67
67
|
'rows.hintStack': 'Brīva kaudze: bez izkārtojumiem — katrs nomestais bloks kļūst par savu pilna platuma sadaļu, sakrautu secībā.',
|
|
68
68
|
'files.libraryCount': 'Bibliotēka — {count} faili',
|
|
69
|
-
'files.openManager': 'Atvērt
|
|
69
|
+
'files.openManager': 'Atvērt multivides bibliotēku',
|
|
70
70
|
'files.dragToCanvas': 'Velciet jebkuru failu tieši uz audekla, lai novietotu to kā attēla bloku.',
|
|
71
71
|
'layers.structure': 'Struktūra',
|
|
72
72
|
'layers.sectionSingle': 'Sadaļa',
|
|
@@ -82,7 +82,7 @@ export const LV = {
|
|
|
82
82
|
'vars.noMatch': 'Neviens mainīgais neatbilst šim filtram.',
|
|
83
83
|
'vars.noneDeclared': 'Jūsu lietotne vēl nav deklarējusi nevienu mainīgo.',
|
|
84
84
|
'vars.rteHint': 'Rediģējot tekstu, jūs varat arī izvēlēties mainīgo tieši no iegultās rīkjoslas nolaižamās izvēlnes.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Multivides bibliotēka',
|
|
86
86
|
'modal.assetsTitle': 'Resursi',
|
|
87
87
|
'modal.replaceImage': 'Aizstāt attēlu',
|
|
88
88
|
'library.searchPlaceholder': 'Meklēt failus…',
|
package/src/core/i18n/nb.js
CHANGED
|
@@ -66,7 +66,7 @@ export const NB = {
|
|
|
66
66
|
'Seksjonsbasert: slipp en layout først, deretter blokker i kolonnene — hver seksjon har egen bakgrunn, kant, flex- eller rutenettinnstillinger.',
|
|
67
67
|
'rows.hintStack': 'Fri stabel: uten layouter — hver blokk du slipper blir sin egen fullbredde-seksjon, stablet i rekkefølge.',
|
|
68
68
|
'files.libraryCount': 'Bibliotek — {count} filer',
|
|
69
|
-
'files.openManager': 'Åpne
|
|
69
|
+
'files.openManager': 'Åpne mediebibliotek',
|
|
70
70
|
'files.dragToCanvas': 'Dra en hvilken som helst fil direkte til lerretet for å plassere den som en bildeblokk.',
|
|
71
71
|
'layers.structure': 'Struktur',
|
|
72
72
|
'layers.sectionSingle': 'Seksjon',
|
|
@@ -82,7 +82,7 @@ export const NB = {
|
|
|
82
82
|
'vars.noMatch': 'Ingen variabel samsvarer med dette filteret.',
|
|
83
83
|
'vars.noneDeclared': 'Applikasjonen din har ennå ikke erklært noen variabler.',
|
|
84
84
|
'vars.rteHint': 'Når du redigerer tekst kan du også velge en variabel direkte fra rullegardinmenyen i den innebygde verktøylinjen.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Mediebibliotek',
|
|
86
86
|
'modal.assetsTitle': 'Ressurser',
|
|
87
87
|
'modal.replaceImage': 'Erstatt bilde',
|
|
88
88
|
'library.searchPlaceholder': 'Søk i filer…',
|
package/src/core/i18n/nl.js
CHANGED
|
@@ -66,7 +66,7 @@ export const NL = {
|
|
|
66
66
|
'Op secties gebaseerd: plaats eerst een lay-out, sleep dan blokken in de kolommen — elke sectie heeft zijn eigen achtergrond, rand, flex- of gridinstellingen.',
|
|
67
67
|
'rows.hintStack': 'Vrije stapel: zonder lay-outs — elk blok dat je plaatst wordt een eigen sectie over de volle breedte, op volgorde gestapeld.',
|
|
68
68
|
'files.libraryCount': 'Bibliotheek — {count} bestanden',
|
|
69
|
-
'files.openManager': '
|
|
69
|
+
'files.openManager': 'Mediabibliotheek openen',
|
|
70
70
|
'files.dragToCanvas': 'Sleep elk bestand direct naar het canvas om het als afbeeldingsblok te plaatsen.',
|
|
71
71
|
'layers.structure': 'Structuur',
|
|
72
72
|
'layers.sectionSingle': 'Sectie',
|
|
@@ -82,7 +82,7 @@ export const NL = {
|
|
|
82
82
|
'vars.noMatch': 'Geen variabele komt overeen met dit filter.',
|
|
83
83
|
'vars.noneDeclared': 'Jouw applicatie heeft nog geen variabelen gedeclareerd.',
|
|
84
84
|
'vars.rteHint': 'Tijdens het bewerken van tekst kun je ook een variabele kiezen via het dropdownmenu van de inline werkbalk.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Mediabibliotheek',
|
|
86
86
|
'modal.assetsTitle': 'Bestanden',
|
|
87
87
|
'modal.replaceImage': 'Afbeelding vervangen',
|
|
88
88
|
'library.searchPlaceholder': 'Bestanden zoeken…',
|
package/src/core/i18n/pl.js
CHANGED
|
@@ -66,7 +66,7 @@ export const PL = {
|
|
|
66
66
|
'Oparty na sekcjach: najpierw upuść układ, potem bloki w jego kolumnach — każda sekcja ma własne tło, obramowanie, ustawienia flex lub siatki.',
|
|
67
67
|
'rows.hintStack': 'Wolny stos: bez układów — każdy upuszczony blok staje się osobną, pełnowymiarową sekcją, ułożoną w kolejności.',
|
|
68
68
|
'files.libraryCount': 'Biblioteka — {count} plików',
|
|
69
|
-
'files.openManager': 'Otwórz
|
|
69
|
+
'files.openManager': 'Otwórz bibliotekę mediów',
|
|
70
70
|
'files.dragToCanvas': 'Przeciągnij dowolny plik bezpośrednio na płótno, aby umieścić go jako blok obrazu.',
|
|
71
71
|
'layers.structure': 'Struktura',
|
|
72
72
|
'layers.sectionSingle': 'Sekcja',
|
|
@@ -82,7 +82,7 @@ export const PL = {
|
|
|
82
82
|
'vars.noMatch': 'Żadna zmienna nie pasuje do tego filtra.',
|
|
83
83
|
'vars.noneDeclared': 'Twoja aplikacja nie zadeklarowała jeszcze żadnych zmiennych.',
|
|
84
84
|
'vars.rteHint': 'Podczas edycji tekstu możesz też wybrać zmienną bezpośrednio z rozwijanego menu paska narzędzi.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Biblioteka mediów',
|
|
86
86
|
'modal.assetsTitle': 'Zasoby',
|
|
87
87
|
'modal.replaceImage': 'Zastąp obraz',
|
|
88
88
|
'library.searchPlaceholder': 'Szukaj plików…',
|
package/src/core/i18n/pt.js
CHANGED
|
@@ -66,7 +66,7 @@ export const PT = {
|
|
|
66
66
|
'Baseado em seções: solte um layout primeiro, depois blocos em suas colunas — cada seção tem seu próprio fundo, borda, configurações flex ou grade.',
|
|
67
67
|
'rows.hintStack': 'Pilha livre: sem layouts — cada bloco que você solta se torna sua própria seção de largura total, empilhada em ordem.',
|
|
68
68
|
'files.libraryCount': 'Biblioteca — {count} arquivos',
|
|
69
|
-
'files.openManager': 'Abrir
|
|
69
|
+
'files.openManager': 'Abrir biblioteca de mídia',
|
|
70
70
|
'files.dragToCanvas': 'Arraste qualquer arquivo diretamente para a tela para colocá-lo como bloco de imagem.',
|
|
71
71
|
'layers.structure': 'Estrutura',
|
|
72
72
|
'layers.sectionSingle': 'Seção',
|
|
@@ -82,7 +82,7 @@ export const PT = {
|
|
|
82
82
|
'vars.noMatch': 'Nenhuma variável corresponde a esse filtro.',
|
|
83
83
|
'vars.noneDeclared': 'Sua aplicação ainda não declarou nenhuma variável.',
|
|
84
84
|
'vars.rteHint': 'Ao editar texto, você também pode escolher uma variável diretamente no menu da barra de ferramentas em linha.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Biblioteca de mídia',
|
|
86
86
|
'modal.assetsTitle': 'Recursos',
|
|
87
87
|
'modal.replaceImage': 'Substituir imagem',
|
|
88
88
|
'library.searchPlaceholder': 'Pesquisar arquivos…',
|
package/src/core/i18n/ro.js
CHANGED
|
@@ -66,7 +66,7 @@ export const RO = {
|
|
|
66
66
|
'Bazat pe secțiuni: plasați mai întâi un aspect, apoi blocuri în coloanele sale — fiecare secțiune are propriul fundal, bordură, setări flex sau grilă.',
|
|
67
67
|
'rows.hintStack': 'Stivă liberă: fără aspecte — fiecare bloc plasat devine propria secțiune pe toată lățimea, stivuită în ordine.',
|
|
68
68
|
'files.libraryCount': 'Bibliotecă — {count} fișiere',
|
|
69
|
-
'files.openManager': 'Deschide
|
|
69
|
+
'files.openManager': 'Deschide biblioteca media',
|
|
70
70
|
'files.dragToCanvas': 'Trageți orice fișier direct pe canvas pentru a-l plasa ca bloc de imagine.',
|
|
71
71
|
'layers.structure': 'Structură',
|
|
72
72
|
'layers.sectionSingle': 'Secțiune',
|
|
@@ -82,7 +82,7 @@ export const RO = {
|
|
|
82
82
|
'vars.noMatch': 'Nicio variabilă nu corespunde acestui filtru.',
|
|
83
83
|
'vars.noneDeclared': 'Aplicația dvs. nu a declarat încă nicio variabilă.',
|
|
84
84
|
'vars.rteHint': 'În timp ce editați text, puteți alege o variabilă și direct din meniul derulant al barei de instrumente în linie.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Biblioteca media',
|
|
86
86
|
'modal.assetsTitle': 'Resurse',
|
|
87
87
|
'modal.replaceImage': 'Înlocuiește imaginea',
|
|
88
88
|
'library.searchPlaceholder': 'Căutare fișiere…',
|
package/src/core/i18n/ru.js
CHANGED
|
@@ -66,7 +66,7 @@ export const RU = {
|
|
|
66
66
|
'На основе секций: сначала поместите макет, затем блоки в его колонки — у каждой секции свой фон, граница, настройки flex или сетки.',
|
|
67
67
|
'rows.hintStack': 'Свободный стек: без макетов — каждый добавленный блок становится собственной секцией на всю ширину, уложенной по порядку.',
|
|
68
68
|
'files.libraryCount': 'Библиотека — {count} файлов',
|
|
69
|
-
'files.openManager': 'Открыть
|
|
69
|
+
'files.openManager': 'Открыть медиатеку',
|
|
70
70
|
'files.dragToCanvas': 'Перетащите любой файл прямо на холст, чтобы разместить его как блок изображения.',
|
|
71
71
|
'layers.structure': 'Структура',
|
|
72
72
|
'layers.sectionSingle': 'Секция',
|
|
@@ -82,7 +82,7 @@ export const RU = {
|
|
|
82
82
|
'vars.noMatch': 'Нет переменных, соответствующих этому фильтру.',
|
|
83
83
|
'vars.noneDeclared': 'Ваше приложение ещё не объявило переменные.',
|
|
84
84
|
'vars.rteHint': 'При редактировании текста вы также можете выбрать переменную прямо из выпадающего списка встроенной панели инструментов.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Медиатека',
|
|
86
86
|
'modal.assetsTitle': 'Файлы',
|
|
87
87
|
'modal.replaceImage': 'Заменить изображение',
|
|
88
88
|
'library.searchPlaceholder': 'Поиск файлов…',
|
package/src/core/i18n/sk.js
CHANGED
|
@@ -66,7 +66,7 @@ export const SK = {
|
|
|
66
66
|
'Založené na sekciách: najprv umiestnite rozloženie, potom bloky do jeho stĺpcov — každá sekcia má vlastné pozadie, ohraničenie, nastavenia flex alebo mriežky.',
|
|
67
67
|
'rows.hintStack': 'Voľný zásobník: bez rozložení — každý umiestnený blok sa stane vlastnou sekciou na celú šírku, zoradenou postupne.',
|
|
68
68
|
'files.libraryCount': 'Knižnica — {count} súborov',
|
|
69
|
-
'files.openManager': 'Otvoriť
|
|
69
|
+
'files.openManager': 'Otvoriť knižnicu médií',
|
|
70
70
|
'files.dragToCanvas': 'Presuňte ľubovoľný súbor priamo na plátno a umiestnite ho ako obrazový blok.',
|
|
71
71
|
'layers.structure': 'Štruktúra',
|
|
72
72
|
'layers.sectionSingle': 'Sekcia',
|
|
@@ -82,7 +82,7 @@ export const SK = {
|
|
|
82
82
|
'vars.noMatch': 'Žiadna premenná nezodpovedá tomuto filtru.',
|
|
83
83
|
'vars.noneDeclared': 'Vaša aplikácia zatiaľ nedeklarovala žiadne premenné.',
|
|
84
84
|
'vars.rteHint': 'Pri úprave textu môžete premennú vybrať aj priamo z rozbaľovacej ponuky vloženého panela nástrojov.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Knižnica médií',
|
|
86
86
|
'modal.assetsTitle': 'Zdroje',
|
|
87
87
|
'modal.replaceImage': 'Nahradiť obrázok',
|
|
88
88
|
'library.searchPlaceholder': 'Hľadať súbory…',
|
package/src/core/i18n/sl.js
CHANGED
|
@@ -66,7 +66,7 @@ export const SL = {
|
|
|
66
66
|
'Temelji na odsekih: najprej spustite postavitev, nato bloke v njene stolpce — vsak odsek ima lastno ozadje, rob, nastavitve flex ali mreže.',
|
|
67
67
|
'rows.hintStack': 'Prosti sklad: brez postavitev — vsak spuščen blok postane lasten odsek čez celo širino, zložen po vrsti.',
|
|
68
68
|
'files.libraryCount': 'Knjižnica — {count} datotek',
|
|
69
|
-
'files.openManager': 'Odpri
|
|
69
|
+
'files.openManager': 'Odpri predstavnostno knjižnico',
|
|
70
70
|
'files.dragToCanvas': 'Povlecite katero koli datoteko neposredno na platno, da jo postavite kot slikovni blok.',
|
|
71
71
|
'layers.structure': 'Struktura',
|
|
72
72
|
'layers.sectionSingle': 'Odsek',
|
|
@@ -82,7 +82,7 @@ export const SL = {
|
|
|
82
82
|
'vars.noMatch': 'Nobena spremenljivka ne ustreza temu filtru.',
|
|
83
83
|
'vars.noneDeclared': 'Vaša aplikacija še ni deklarirala nobenih spremenljivk.',
|
|
84
84
|
'vars.rteHint': 'Med urejanjem besedila lahko spremenljivko izberete tudi neposredno iz spustnega menija vgrajene orodne vrstice.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Predstavnostna knjižnica',
|
|
86
86
|
'modal.assetsTitle': 'Viri',
|
|
87
87
|
'modal.replaceImage': 'Zamenjaj sliko',
|
|
88
88
|
'library.searchPlaceholder': 'Iskanje datotek…',
|
package/src/core/i18n/sv.js
CHANGED
|
@@ -66,7 +66,7 @@ export const SV = {
|
|
|
66
66
|
'Sektionsbaserad: släpp en layout först, sedan block i dess kolumner — varje sektion har egen bakgrund, kant, flex- eller rutnätsinställningar.',
|
|
67
67
|
'rows.hintStack': 'Fri stapel: utan layouter — varje block du släpper blir sin egen sektion i full bredd, staplad i ordning.',
|
|
68
68
|
'files.libraryCount': 'Bibliotek — {count} filer',
|
|
69
|
-
'files.openManager': 'Öppna
|
|
69
|
+
'files.openManager': 'Öppna mediebiblioteket',
|
|
70
70
|
'files.dragToCanvas': 'Dra valfri fil direkt till canvasen för att placera den som ett bildblock.',
|
|
71
71
|
'layers.structure': 'Struktur',
|
|
72
72
|
'layers.sectionSingle': 'Sektion',
|
|
@@ -82,7 +82,7 @@ export const SV = {
|
|
|
82
82
|
'vars.noMatch': 'Ingen variabel matchar det filtret.',
|
|
83
83
|
'vars.noneDeclared': 'Din applikation har ännu inte deklarerat några variabler.',
|
|
84
84
|
'vars.rteHint': 'När du redigerar text kan du också välja en variabel direkt från rullgardinsmenyn i den inbäddade verktygsfältet.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Mediebibliotek',
|
|
86
86
|
'modal.assetsTitle': 'Resurser',
|
|
87
87
|
'modal.replaceImage': 'Ersätt bild',
|
|
88
88
|
'library.searchPlaceholder': 'Sök filer…',
|
package/src/core/i18n/tr.js
CHANGED
|
@@ -66,7 +66,7 @@ export const TR = {
|
|
|
66
66
|
'Bölüm tabanlı: önce bir düzen bırakın, ardından sütunlarına bloklar bırakın — her bölümün kendi arka planı, kenarlığı, flex veya ızgara ayarları vardır.',
|
|
67
67
|
'rows.hintStack': 'Serbest yığın: düzen olmadan — bıraktığınız her blok, sırayla istiflenen kendi tam genişlikteki bölümü haline gelir.',
|
|
68
68
|
'files.libraryCount': 'Kitaplık — {count} dosya',
|
|
69
|
-
'files.openManager': '
|
|
69
|
+
'files.openManager': 'Medya kitaplığını aç',
|
|
70
70
|
'files.dragToCanvas': 'Görsel blok olarak yerleştirmek için herhangi bir dosyayı doğrudan tuvale sürükleyin.',
|
|
71
71
|
'layers.structure': 'Yapı',
|
|
72
72
|
'layers.sectionSingle': 'Bölüm',
|
|
@@ -82,7 +82,7 @@ export const TR = {
|
|
|
82
82
|
'vars.noMatch': 'Bu filtreyle eşleşen değişken yok.',
|
|
83
83
|
'vars.noneDeclared': 'Uygulamanız henüz herhangi bir değişken tanımlamadı.',
|
|
84
84
|
'vars.rteHint': 'Metni düzenlerken, satır içi araç çubuğunun açılır menüsünden doğrudan bir değişken de seçebilirsiniz.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Medya kitaplığı',
|
|
86
86
|
'modal.assetsTitle': 'Varlıklar',
|
|
87
87
|
'modal.replaceImage': 'Görseli değiştir',
|
|
88
88
|
'library.searchPlaceholder': 'Dosyaları ara…',
|
package/src/core/i18n/uk.js
CHANGED
|
@@ -66,7 +66,7 @@ export const UK = {
|
|
|
66
66
|
'На основі секцій: спочатку розмістіть макет, потім блоки в його колонках — кожна секція має власний фон, межу, налаштування flex або сітки.',
|
|
67
67
|
'rows.hintStack': 'Вільний стек: без макетів — кожен доданий блок стає власною секцією на всю ширину, укладеною по порядку.',
|
|
68
68
|
'files.libraryCount': 'Бібліотека — {count} файлів',
|
|
69
|
-
'files.openManager': 'Відкрити
|
|
69
|
+
'files.openManager': 'Відкрити медіатеку',
|
|
70
70
|
'files.dragToCanvas': 'Перетягніть будь-який файл прямо на полотно, щоб розмістити його як блок зображення.',
|
|
71
71
|
'layers.structure': 'Структура',
|
|
72
72
|
'layers.sectionSingle': 'Секція',
|
|
@@ -82,7 +82,7 @@ export const UK = {
|
|
|
82
82
|
'vars.noMatch': 'Немає змінних, що відповідають цьому фільтру.',
|
|
83
83
|
'vars.noneDeclared': 'Ваш застосунок ще не оголосив жодних змінних.',
|
|
84
84
|
'vars.rteHint': 'Під час редагування тексту ви також можете вибрати змінну прямо з випадного списку вбудованої панелі інструментів.',
|
|
85
|
-
'modal.fileManager': '
|
|
85
|
+
'modal.fileManager': 'Медіатека',
|
|
86
86
|
'modal.assetsTitle': 'Файли',
|
|
87
87
|
'modal.replaceImage': 'Замінити зображення',
|
|
88
88
|
'library.searchPlaceholder': 'Пошук файлів…',
|
package/src/core/icons.js
CHANGED
package/src/core/import-html.js
CHANGED
|
@@ -442,6 +442,21 @@ function logicMarkersOf(text) {
|
|
|
442
442
|
return out;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/** Whether an element (or the transparent single-child chain under it) carries its own padding -- the shape the exporter writes for a block's py/px, and a builder section's own spacing. Such a wrapper is one block, never part of a text run. */
|
|
446
|
+
function hasOwnRunPad(el) {
|
|
447
|
+
let e = el;
|
|
448
|
+
if (e.style && paddingOf(e.style)) return true;
|
|
449
|
+
while (
|
|
450
|
+
e.children && e.children.length === 1
|
|
451
|
+
&& !INLINE_TAGS.test(e.firstElementChild.tagName)
|
|
452
|
+
&& (e.textContent || '') === (e.firstElementChild.textContent || '')
|
|
453
|
+
) {
|
|
454
|
+
e = e.firstElementChild;
|
|
455
|
+
if (e.style && paddingOf(e.style)) return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
445
460
|
function blocksFromNodes(nodes) {
|
|
446
461
|
const out = [];
|
|
447
462
|
let buf = [];
|
|
@@ -458,6 +473,11 @@ function blocksFromNodes(nodes) {
|
|
|
458
473
|
// first-element-only read never saw. Per-element differences inside
|
|
459
474
|
// the run survive as the inline styles `cleanImportHtml` now keeps.
|
|
460
475
|
let baseEl = INLINE_TAGS.test(bufFirstEl.tagName) ? (bufFirstEl.parentElement || bufFirstEl) : bufFirstEl;
|
|
476
|
+
// The run's own spacing: the exporter writes a text block's py/px as
|
|
477
|
+
// padding on the wrapper div the descent below walks straight past
|
|
478
|
+
// (it lands on the <p>), so a padded block came back at the default
|
|
479
|
+
// 10px/0 on every save. First padding on the wrapper chain wins.
|
|
480
|
+
let runPad = paddingOf(baseEl.style);
|
|
461
481
|
// Then descend through transparent single-child wrappers: builders
|
|
462
482
|
// nest a `font-family:sans-serif` shim div around the div that
|
|
463
483
|
// carries the real typography, and `inheritedStyle` below walks *up*
|
|
@@ -467,7 +487,8 @@ function blocksFromNodes(nodes) {
|
|
|
467
487
|
baseEl.children.length === 1
|
|
468
488
|
&& !INLINE_TAGS.test(baseEl.firstElementChild.tagName)
|
|
469
489
|
&& (baseEl.textContent || '') === (baseEl.firstElementChild.textContent || '')
|
|
470
|
-
) baseEl = baseEl.firstElementChild;
|
|
490
|
+
) { baseEl = baseEl.firstElementChild; if (!runPad) runPad = paddingOf(baseEl.style); }
|
|
491
|
+
if (runPad) { over.py = runPad.py; over.px = runPad.px; }
|
|
471
492
|
const size = fontPx(inheritedStyle(baseEl, 'fontSize')); if (size) over.size = size;
|
|
472
493
|
const color = inheritedStyle(baseEl, 'color'); if (color) over.color = color;
|
|
473
494
|
over.align = textAlignOf(baseEl);
|
|
@@ -513,8 +534,14 @@ function blocksFromNodes(nodes) {
|
|
|
513
534
|
return;
|
|
514
535
|
}
|
|
515
536
|
if (isStructural(target)) { flush(); out.push(blk('html', { code: n.outerHTML })); return; }
|
|
537
|
+
// A wrapper carrying its own padding is a block boundary, not part of a
|
|
538
|
+
// run: the exporter writes every text block as exactly such a padded div,
|
|
539
|
+
// and buffering two of them together merged neighbouring blocks into one
|
|
540
|
+
// -- the second lost its padding, size, everything -- on every save.
|
|
541
|
+
if (n.nodeType === 1 && !INLINE_TAGS.test(target.tagName) && hasOwnRunPad(target) && buf.length) flush();
|
|
516
542
|
if (!bufFirstEl) bufFirstEl = target;
|
|
517
543
|
buf.push(n.outerHTML);
|
|
544
|
+
if (n.nodeType === 1 && !INLINE_TAGS.test(target.tagName) && hasOwnRunPad(target)) flush();
|
|
518
545
|
});
|
|
519
546
|
flush();
|
|
520
547
|
return out;
|
|
@@ -570,16 +597,26 @@ function isPassthroughTable(tb) {
|
|
|
570
597
|
return cells.length === 1;
|
|
571
598
|
}
|
|
572
599
|
|
|
600
|
+
/** CSSOM hands colors back as `rgb(r, g, b)` even when the source (and the
|
|
601
|
+
* user's own value) was hex -- so every save visibly rewrote colour fields.
|
|
602
|
+
* Solid rgb() flattens back to hex; anything else (rgba, keywords,
|
|
603
|
+
* `transparent`) passes through untouched. */
|
|
604
|
+
function hexOf(value) {
|
|
605
|
+
const m = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(String(value == null ? '' : value).trim());
|
|
606
|
+
if (!m) return value;
|
|
607
|
+
return '#' + [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, '0')).join('');
|
|
608
|
+
}
|
|
609
|
+
|
|
573
610
|
function bgOf(el) {
|
|
574
611
|
const st = el.style;
|
|
575
|
-
if (st && st.backgroundColor) return st.backgroundColor;
|
|
612
|
+
if (st && st.backgroundColor) return hexOf(st.backgroundColor);
|
|
576
613
|
// The `background` shorthand is only a usable *color* when it holds no
|
|
577
614
|
// image/gradient -- browsers populate `backgroundColor` from a shorthand
|
|
578
615
|
// that includes a color, so reaching here with a url() means there is no
|
|
579
616
|
// color to take (and taking the raw string set broken values).
|
|
580
617
|
const short = (st && st.background) || '';
|
|
581
|
-
if (short && short.indexOf('url(') < 0 && short.indexOf('gradient(') < 0) return short;
|
|
582
|
-
return (el.getAttribute && el.getAttribute('bgcolor')) || '';
|
|
618
|
+
if (short && short.indexOf('url(') < 0 && short.indexOf('gradient(') < 0) return hexOf(short);
|
|
619
|
+
return hexOf((el.getAttribute && el.getAttribute('bgcolor')) || '');
|
|
583
620
|
}
|
|
584
621
|
|
|
585
622
|
function padOf(el) {
|
|
@@ -604,7 +641,7 @@ function applyPad(rows, pad) {
|
|
|
604
641
|
|
|
605
642
|
/** Longhand side colors are always a single value; the `borderColor` shorthand reads back as a per-side list when the sides differ (`rgb(...) rgb(...) currentcolor`), which is not a usable color. */
|
|
606
643
|
function borderColorOf(st) {
|
|
607
|
-
const ok = (v) => (v && v !== 'currentcolor' ? v : '');
|
|
644
|
+
const ok = (v) => (v && v !== 'currentcolor' ? hexOf(v) : '');
|
|
608
645
|
return ok(st.borderTopColor) || ok(st.borderLeftColor) || ok(st.borderRightColor) || ok(st.borderBottomColor) || '';
|
|
609
646
|
}
|
|
610
647
|
|
|
@@ -940,7 +977,7 @@ function collectRows(nodes) {
|
|
|
940
977
|
function themeFromParsedDoc(doc) {
|
|
941
978
|
const theme = {};
|
|
942
979
|
const body = doc.body;
|
|
943
|
-
let bg = (body.style && (body.style.backgroundColor || body.style.background)) || body.getAttribute('bgcolor') || '';
|
|
980
|
+
let bg = hexOf((body.style && (body.style.backgroundColor || body.style.background)) || body.getAttribute('bgcolor') || '');
|
|
944
981
|
if (!bg) {
|
|
945
982
|
const outer = body.querySelector('table');
|
|
946
983
|
if (outer) bg = bgOf(outer) || (outer.querySelector('td') ? bgOf(outer.querySelector('td')) : '');
|
|
@@ -986,12 +1023,26 @@ function themeFromParsedDoc(doc) {
|
|
|
986
1023
|
theme.borderStyle = borderStyleOf(content.style);
|
|
987
1024
|
theme.borderColor = borderColorOf(content.style) || '';
|
|
988
1025
|
}
|
|
1026
|
+
const sh = content.style && content.style.boxShadow;
|
|
1027
|
+
if (sh && sh !== 'none') theme.shadow = sh;
|
|
1028
|
+
// CLAIMED MEANS CONSUMED. These styles now live on the theme; leaving
|
|
1029
|
+
// them on the node let the row walker's card-folding (applyFrame /
|
|
1030
|
+
// applyPad) absorb the very same border, radius, shadow and page
|
|
1031
|
+
// padding into the rows -- so one save doubled every frame the user
|
|
1032
|
+
// had set at canvas level. htmlToDoc runs this pass before the row
|
|
1033
|
+
// pass for exactly this reason.
|
|
1034
|
+
if (content.style) {
|
|
1035
|
+
content.style.border = '';
|
|
1036
|
+
content.style.borderRadius = '';
|
|
1037
|
+
content.style.boxShadow = '';
|
|
1038
|
+
}
|
|
989
1039
|
const cell = content.closest ? content.closest('td') : null;
|
|
990
1040
|
if (cell && cell.style) {
|
|
991
1041
|
const padY = PX(cell.style.paddingTop);
|
|
992
1042
|
const padX = PX(cell.style.paddingLeft);
|
|
993
1043
|
if (padY > 0) theme.padY = padY;
|
|
994
1044
|
if (padX > 0) theme.padX = padX;
|
|
1045
|
+
cell.style.padding = '';
|
|
995
1046
|
}
|
|
996
1047
|
}
|
|
997
1048
|
}
|
|
@@ -1018,8 +1069,8 @@ function themeFromParsedDoc(doc) {
|
|
|
1018
1069
|
linkCounts[c] = (linkCounts[c] || 0) + 1;
|
|
1019
1070
|
});
|
|
1020
1071
|
const link = Object.keys(linkCounts).sort((a, b) => linkCounts[b] - linkCounts[a])[0];
|
|
1021
|
-
if (link) theme.link = link;
|
|
1022
|
-
if (body.style && body.style.color) theme.text = body.style.color;
|
|
1072
|
+
if (link) theme.link = hexOf(link);
|
|
1073
|
+
if (body.style && body.style.color) theme.text = hexOf(body.style.color);
|
|
1023
1074
|
return theme;
|
|
1024
1075
|
}
|
|
1025
1076
|
|
|
@@ -1057,7 +1108,10 @@ export function htmlToDoc(src) {
|
|
|
1057
1108
|
// (never-inlined exports, hand-written emails) classify like inlined ones.
|
|
1058
1109
|
// Best-effort: a pathological stylesheet must never block the import.
|
|
1059
1110
|
try { inlineStylesheets(doc); } catch { /* proceed with inline styles only */ }
|
|
1060
|
-
|
|
1111
|
+
// Theme first: themeFromParsedDoc consumes the styles it claims off the
|
|
1112
|
+
// scaffold nodes, and the row walker must see the cleaned DOM.
|
|
1113
|
+
const theme = themeFromParsedDoc(doc);
|
|
1114
|
+
return { rows: collectRows(Array.from(doc.body.childNodes)), theme };
|
|
1061
1115
|
}
|
|
1062
1116
|
|
|
1063
1117
|
export function htmlToRows(src) {
|
package/src/core/sanitize.js
CHANGED
|
@@ -131,6 +131,40 @@ export const cssUrl = (u) => {
|
|
|
131
131
|
return raw.replace(/["'()\\]|\s/g, (c) => CSS_URL_ESCAPE[c] || '%20');
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A link target on its way into an anchor. Two jobs, and both of them showed
|
|
136
|
+
* up as "the link does not work" rather than as anything that errors:
|
|
137
|
+
*
|
|
138
|
+
* - A bare host (`selise.ch`, `www.selise.ch/pricing`) gets `https://`.
|
|
139
|
+
* Typed with no scheme it is a *relative* URL, so the mail client
|
|
140
|
+
* resolves it against its own origin and the click lands nowhere -- the
|
|
141
|
+
* single most common way an authored link ships broken, and invisible in
|
|
142
|
+
* the editor because the canvas never follows it.
|
|
143
|
+
* - Schemes are allowlisted the way `cssUrl` allowlists image sources, so
|
|
144
|
+
* `javascript:` resolves to nothing instead of becoming a click target.
|
|
145
|
+
*
|
|
146
|
+
* Left exactly as typed: anything already carrying an allowed scheme, an
|
|
147
|
+
* in-page `#anchor`, a rooted `/path`, and anything opening with a merge tag
|
|
148
|
+
* -- `{{ResetUrl}}` is a whole URL the host substitutes later, so prefixing
|
|
149
|
+
* it would corrupt what reaches the template engine. A bare `name@host.tld`
|
|
150
|
+
* becomes `mailto:`, which is the only thing it could have meant.
|
|
151
|
+
*/
|
|
152
|
+
export const linkHref = (u) => {
|
|
153
|
+
const raw = String(u == null ? '' : u).trim();
|
|
154
|
+
if (!raw) return '';
|
|
155
|
+
const scheme = raw.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
156
|
+
if (scheme) {
|
|
157
|
+
if (/^(https?|mailto|tel)$/i.test(scheme[1])) return raw;
|
|
158
|
+
// `selise.ch:8080/x` matches the scheme shape but is a host and a port;
|
|
159
|
+
// it falls through to be prefixed rather than dropped.
|
|
160
|
+
if (!/^[a-z][a-z0-9+.-]*:\d/i.test(raw)) return '';
|
|
161
|
+
}
|
|
162
|
+
if (raw.startsWith('//')) return 'https:' + raw;
|
|
163
|
+
if (/^[#/]/.test(raw) || raw.startsWith('{{')) return raw;
|
|
164
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) return 'mailto:' + raw;
|
|
165
|
+
return 'https://' + raw;
|
|
166
|
+
};
|
|
167
|
+
|
|
134
168
|
export const escHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
135
169
|
|
|
136
170
|
/** Minimal HTML syntax highlighter for the code view. */
|
package/src/core/theme.js
CHANGED
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* accept the literal `transparent` (and any rgba()/#rrggbbaa value) as well
|
|
13
13
|
* as a hex colour.
|
|
14
14
|
*/
|
|
15
|
-
export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, borderW: 0, borderStyle: 'solid', borderColor: '#e2e2e5', font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
|
|
15
|
+
export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, borderW: 0, borderStyle: 'solid', borderColor: '#e2e2e5', shadow: '', font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
|
package/src/mailcraft-editor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { renderDoc } from './render/canvas.js';
|
|
|
6
6
|
import { renderRte } from './render/rte.js';
|
|
7
7
|
import { renderField, renderFieldCards, typeCommit } from './render/fields.js';
|
|
8
8
|
import { icon } from './core/icons.js';
|
|
9
|
+
import { brandLockup } from './core/brand.js';
|
|
9
10
|
import { TOKEN } from './core/variables.js';
|
|
10
11
|
import { hl, cssUrl } from './core/sanitize.js';
|
|
11
12
|
import { decorateLogicTags } from './core/export.js';
|
|
@@ -494,7 +495,15 @@ export class MailCraftEditor extends ElementBase {
|
|
|
494
495
|
async copyScreenshot(blob) {
|
|
495
496
|
const t = this.core.t;
|
|
496
497
|
try {
|
|
497
|
-
|
|
498
|
+
// Safari only honours `clipboard.write()` while the user gesture is
|
|
499
|
+
// still live, and awaiting the capture first (per-image fetch +
|
|
500
|
+
// inline, decode, rasterize, PNG encode) always outlives it --
|
|
501
|
+
// NotAllowedError on every attempt, where Chromium is lenient and
|
|
502
|
+
// lets it through. Handing ClipboardItem the *promise* is the
|
|
503
|
+
// supported shape for exactly this: the write is issued
|
|
504
|
+
// synchronously with the gesture and the capture resolves inside
|
|
505
|
+
// it. Chromium accepts the same form, so there is no branch here.
|
|
506
|
+
const png = blob ? Promise.resolve(blob) : this.screenshotPng();
|
|
498
507
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
|
|
499
508
|
this.core.flash(t('toast.pngCopied'));
|
|
500
509
|
} catch {
|
|
@@ -569,15 +578,11 @@ export class MailCraftEditor extends ElementBase {
|
|
|
569
578
|
const t = this.core.t;
|
|
570
579
|
const header = elS('header', "display: flex; align-items: center; gap: 14px; padding: 0 16px; border-bottom: 1px solid var(--ed-line); background: linear-gradient(to bottom, var(--ed-panel), var(--ed-panel-2)); position: relative; z-index: 30;", { class: 'mc-header' });
|
|
571
580
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
brand.append(
|
|
578
|
-
brandMark,
|
|
579
|
-
elS('span', 'font-family: var(--ed-font); font-weight: 600; font-size: 15px; letter-spacing: -0.01em; line-height: 1;', { text: 'MailCraft', class: 'mc-brand-name' }),
|
|
580
|
-
);
|
|
581
|
+
// The logo itself, not a mark beside a typeset word: the wordmark is part
|
|
582
|
+
// of the artwork, so it keeps the brand's own typeface whatever font the
|
|
583
|
+
// host pushes into --ed-font.
|
|
584
|
+
const brand = elS('div', 'display: flex; align-items: center;', { class: 'mc-brand' });
|
|
585
|
+
brand.appendChild(brandLockup(30));
|
|
581
586
|
if (on.logo) header.append(brand, elS('div', 'width: 1px; height: 22px; background: var(--ed-line);'));
|
|
582
587
|
|
|
583
588
|
this.savedLabel = elS('span', 'display: flex; align-items: center; gap: 6px; font-family: ui-monospace, monospace; font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ed-faint); white-space: nowrap;');
|
|
@@ -1224,6 +1229,11 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1224
1229
|
tile.addEventListener('mouseleave', () => { tile.style.borderColor = 'var(--ed-line)'; });
|
|
1225
1230
|
tile.addEventListener('dragstart', this.core.startDrag({ kind: 'asset', assetId: a.id }));
|
|
1226
1231
|
tile.addEventListener('dragend', () => { this.core.drag = null; this.core.setState({ drop: null, rowDrop: null }); });
|
|
1232
|
+
// Touch Safari never fires HTML5 drag events, so drag alone left
|
|
1233
|
+
// these tiles unusable on iOS. Click is the same fallback the
|
|
1234
|
+
// library modal's tiles already carry; a completed drag ends in
|
|
1235
|
+
// `dragend`, not `click`, so the two do not both fire.
|
|
1236
|
+
tile.addEventListener('click', () => this.core.useAsset(a));
|
|
1227
1237
|
tile.appendChild(elS('div', `width: 100%; height: 64px; background-image: url("${a.url}"); background-size: cover; background-position: center; background-color: var(--ed-work);`, { role: 'img', 'aria-label': a.name }));
|
|
1228
1238
|
tile.appendChild(elS('div', 'font-family: ui-monospace, monospace; font-size: 9px; color: var(--ed-muted); padding: 4px 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', { text: a.name }));
|
|
1229
1239
|
grid.appendChild(tile);
|
package/src/render/block-body.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { icon, brandIcon, socialKey, SOCIAL_BRAND, contrastInk } from '../core/icons.js';
|
|
2
2
|
import { pad } from '../core/layout-style.js';
|
|
3
3
|
import { parseItems, cellsOf } from '../core/parse.js';
|
|
4
|
+
import { linkHref } from '../core/sanitize.js';
|
|
4
5
|
|
|
5
6
|
function el(tag, style, attrs) {
|
|
6
7
|
const node = document.createElement(tag);
|
|
@@ -77,12 +78,17 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
77
78
|
}
|
|
78
79
|
case 'image': {
|
|
79
80
|
const wrap = el('div', { padding: pad(p), textAlign: p.align, fontSize: '0' }, attr);
|
|
80
|
-
|
|
81
|
+
const imgHref = linkHref(p.href);
|
|
82
|
+
if (imgHref) {
|
|
81
83
|
// The % width must live on the anchor, not the img: a percentage on a
|
|
82
84
|
// child of a shrink-to-fit inline-block resolves against the image's
|
|
83
85
|
// own intrinsic size (i.e. not at all), which rendered every linked
|
|
84
86
|
// logo/icon at full intrinsic width no matter what `width` said.
|
|
85
|
-
const a = el('a', { display: 'inline-block', width: p.width + '%' }, { href:
|
|
87
|
+
const a = el('a', { display: 'inline-block', width: p.width + '%' }, { href: imgHref });
|
|
88
|
+
// Same guard as every other anchor the canvas draws: without it a
|
|
89
|
+
// click on a linked logo navigates the host application away from the
|
|
90
|
+
// editor, taking the uncommitted document with it.
|
|
91
|
+
a.addEventListener('click', (e) => e.preventDefault());
|
|
86
92
|
a.appendChild(el('img', { width: '100%', borderRadius: p.radius + 'px', display: 'block', border: '0' }, { src: p.src, alt: p.alt }));
|
|
87
93
|
wrap.appendChild(a);
|
|
88
94
|
} else {
|
|
@@ -100,7 +106,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
100
106
|
// email pattern; the color falls back to the label color so a bare
|
|
101
107
|
// "outline thickness" bump looks right without a second step.
|
|
102
108
|
border: p.borderW ? p.borderW + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.borderColor || p.color) : '0',
|
|
103
|
-
}, { href: p.href, ...editableAttrs(live), text: p.label });
|
|
109
|
+
}, { href: linkHref(p.href), ...editableAttrs(live), text: p.label });
|
|
104
110
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
105
111
|
// Button editing is deliberately plain: no focus-tracked `editing` state, no
|
|
106
112
|
// paste handling, no floating RTE toolbar -- only its label commits on blur.
|
|
@@ -139,7 +145,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
139
145
|
borderRadius: shape === 'circle' ? '50%' : shape === 'square' ? Math.round(box * 0.28) + 'px' : '0',
|
|
140
146
|
color: iconColor, background: badge ? source : 'transparent',
|
|
141
147
|
border: shape === 'outline' ? '1px solid ' + source : '0',
|
|
142
|
-
}, { href: it.href, title: it.label });
|
|
148
|
+
}, { href: linkHref(it.href), title: it.label });
|
|
143
149
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
144
150
|
let provided = null;
|
|
145
151
|
if (ctx && ctx.iconProvider) { try { provided = ctx.iconProvider(key, { label: it.label, size: p.size, color: iconColor }); } catch { provided = null; } }
|
|
@@ -154,7 +160,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
154
160
|
}
|
|
155
161
|
case 'video': {
|
|
156
162
|
const wrap = el('div', { padding: '4px 0', textAlign: 'center' }, attr);
|
|
157
|
-
const a = el('a', { display: 'block', position: 'relative' }, { href: p.href });
|
|
163
|
+
const a = el('a', { display: 'block', position: 'relative' }, { href: linkHref(p.href) });
|
|
158
164
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
159
165
|
a.appendChild(el('img', { width: '100%', display: 'block', border: '0' }, { src: p.src, alt: p.caption }));
|
|
160
166
|
const badge = el('span', { position: 'absolute', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' });
|
|
@@ -197,7 +203,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
197
203
|
case 'menu': {
|
|
198
204
|
const wrap = el('div', { textAlign: p.align, padding: '10px 0' }, attr);
|
|
199
205
|
parseItems(p.items).forEach((it) => {
|
|
200
|
-
const a = el('a', { color: p.color, fontSize: p.size + 'px', fontFamily: p.fontFamily || t.font, textDecoration: 'none', margin: '0 ' + p.gap / 2 + 'px', letterSpacing: '0.12em', textTransform: 'uppercase' }, { href: it.href, text: it.label });
|
|
206
|
+
const a = el('a', { color: p.color, fontSize: p.size + 'px', fontFamily: p.fontFamily || t.font, textDecoration: 'none', margin: '0 ' + p.gap / 2 + 'px', letterSpacing: '0.12em', textTransform: 'uppercase' }, { href: linkHref(it.href), text: it.label });
|
|
201
207
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
202
208
|
wrap.appendChild(a);
|
|
203
209
|
});
|