valtech-components 4.0.972 → 4.0.974
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/esm2022/lib/components/organisms/rich-editor/rich-editor.component.mjs +71 -8
- package/esm2022/lib/components/organisms/rich-editor/rich-editor.i18n.mjs +3 -1
- package/esm2022/lib/services/firebase/storage.service.mjs +8 -3
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +76 -7
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/rich-editor/rich-editor.component.d.ts +13 -1
- package/lib/components/organisms/rich-editor/rich-editor.i18n.d.ts +2 -0
- package/lib/services/firebase/storage.service.d.ts +8 -1
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -70,7 +70,7 @@ import fixWebmDuration from 'fix-webm-duration';
|
|
|
70
70
|
* Current version of valtech-components.
|
|
71
71
|
* This is automatically updated during the publish process.
|
|
72
72
|
*/
|
|
73
|
-
const VERSION = '4.0.
|
|
73
|
+
const VERSION = '4.0.974';
|
|
74
74
|
|
|
75
75
|
function evaluateValtechAccess(rule, context, features = {}, visitedFeatures = new Set()) {
|
|
76
76
|
if (rule == null)
|
|
@@ -5702,15 +5702,20 @@ class StorageService {
|
|
|
5702
5702
|
* Elimina un archivo.
|
|
5703
5703
|
*
|
|
5704
5704
|
* @param path - Ruta del archivo a eliminar
|
|
5705
|
+
* @param options.skipPrefix - Si es true, no aplica el prefix de appId (para
|
|
5706
|
+
* borrar un archivo subido con `uploadAndGetUrl(path, file, { skipPrefix:
|
|
5707
|
+
* true })` hay que pasar el mismo flag, o el path queda mal armado y el
|
|
5708
|
+
* delete falla contra un objeto que no existe).
|
|
5705
5709
|
*
|
|
5706
5710
|
* @example
|
|
5707
5711
|
* ```typescript
|
|
5708
5712
|
* await storage.delete('images/old-photo.jpg');
|
|
5713
|
+
* await storage.delete('apps/merotz/orgs/x/public/images/parts/y/z.png', { skipPrefix: true });
|
|
5709
5714
|
* ```
|
|
5710
5715
|
*/
|
|
5711
|
-
async delete(path) {
|
|
5716
|
+
async delete(path, options) {
|
|
5712
5717
|
try {
|
|
5713
|
-
const prefixedPath = this.prefixStoragePath(path);
|
|
5718
|
+
const prefixedPath = this.prefixStoragePath(path, options?.skipPrefix);
|
|
5714
5719
|
const storageRef = ref(this.storage, prefixedPath);
|
|
5715
5720
|
await deleteObject(storageRef);
|
|
5716
5721
|
}
|
|
@@ -39420,6 +39425,7 @@ const RICH_EDITOR_I18N = {
|
|
|
39420
39425
|
orderedList: 'Lista numerada',
|
|
39421
39426
|
quote: 'Cita',
|
|
39422
39427
|
separator: 'Separador',
|
|
39428
|
+
image: 'Insertar imagen',
|
|
39423
39429
|
},
|
|
39424
39430
|
en: {
|
|
39425
39431
|
bold: 'Bold',
|
|
@@ -39430,6 +39436,7 @@ const RICH_EDITOR_I18N = {
|
|
|
39430
39436
|
orderedList: 'Numbered list',
|
|
39431
39437
|
quote: 'Quote',
|
|
39432
39438
|
separator: 'Separator',
|
|
39439
|
+
image: 'Insert image',
|
|
39433
39440
|
},
|
|
39434
39441
|
};
|
|
39435
39442
|
|
|
@@ -39682,7 +39689,7 @@ function articleToTiptapDoc(article) {
|
|
|
39682
39689
|
// Íconos de la toolbar. B/I/H2/H3 no tienen ícono Ionicons dedicado — se
|
|
39683
39690
|
// renderizan como texto ("B", "I", "H2", "H3") directo en el botón, mismo
|
|
39684
39691
|
// criterio que un editor de texto rico convencional (Google Docs, Notion).
|
|
39685
|
-
addIcons({ listOutline, chatboxOutline, removeOutline });
|
|
39692
|
+
addIcons({ listOutline, chatboxOutline, removeOutline, imageOutline });
|
|
39686
39693
|
/**
|
|
39687
39694
|
* val-rich-editor
|
|
39688
39695
|
*
|
|
@@ -39708,11 +39715,13 @@ class RichEditorComponent {
|
|
|
39708
39715
|
this.props = {};
|
|
39709
39716
|
this.articleChange = new EventEmitter();
|
|
39710
39717
|
this.i18n = inject(I18nService);
|
|
39718
|
+
this.injector = inject(Injector);
|
|
39711
39719
|
/** Se recalcula en cada `onUpdate`/`onSelectionUpdate` para reflejar el
|
|
39712
39720
|
* mark/nodo activo bajo el cursor (ej. el botón "B" resaltado si el
|
|
39713
39721
|
* cursor está dentro de texto en negrita). */
|
|
39714
39722
|
this._toolbarTick = signal(0);
|
|
39715
39723
|
this.toolbarButtons = signal([]);
|
|
39724
|
+
this.uploadingImage = signal(false);
|
|
39716
39725
|
this.i18n.registerDefaults('RichEditor', RICH_EDITOR_I18N);
|
|
39717
39726
|
}
|
|
39718
39727
|
t(key) {
|
|
@@ -39843,6 +39852,36 @@ class RichEditorComponent {
|
|
|
39843
39852
|
}
|
|
39844
39853
|
this.refreshToolbar();
|
|
39845
39854
|
}
|
|
39855
|
+
/**
|
|
39856
|
+
* Sube la imagen elegida vía `StorageService` (Regla #12 — nunca URL pegada
|
|
39857
|
+
* a mano, siempre vía Storage) y la inserta en el punto del cursor.
|
|
39858
|
+
* `StorageService` se resuelve perezosamente (vía `Injector`, no como
|
|
39859
|
+
* campo de la clase): si la app consumer no configuró
|
|
39860
|
+
* `provideValtechFirebase`, el editor sigue funcionando normal para todo
|
|
39861
|
+
* lo demás — solo el botón de imagen queda sin efecto, con log de error en
|
|
39862
|
+
* vez de romper el resto del componente.
|
|
39863
|
+
*/
|
|
39864
|
+
async onImageSelected(event) {
|
|
39865
|
+
const input = event.target;
|
|
39866
|
+
const file = input.files?.[0];
|
|
39867
|
+
input.value = ''; // permite re-seleccionar el mismo archivo después
|
|
39868
|
+
if (!file || !this.editor)
|
|
39869
|
+
return;
|
|
39870
|
+
this.uploadingImage.set(true);
|
|
39871
|
+
try {
|
|
39872
|
+
const storage = this.injector.get(StorageService);
|
|
39873
|
+
const path = `rich-editor/${Date.now()}-${file.name}`;
|
|
39874
|
+
const result = await storage.uploadAndGetUrl(path, file);
|
|
39875
|
+
this.editor.chain().focus().setImage({ src: result.downloadUrl, alt: file.name }).run();
|
|
39876
|
+
this.emitArticle(this.editor.getJSON());
|
|
39877
|
+
}
|
|
39878
|
+
catch (err) {
|
|
39879
|
+
console.error('[val-rich-editor] No se pudo subir la imagen — ¿la app configuró provideValtechFirebase?', err);
|
|
39880
|
+
}
|
|
39881
|
+
finally {
|
|
39882
|
+
this.uploadingImage.set(false);
|
|
39883
|
+
}
|
|
39884
|
+
}
|
|
39846
39885
|
ngOnDestroy() {
|
|
39847
39886
|
this.editor?.destroy();
|
|
39848
39887
|
}
|
|
@@ -39889,15 +39928,30 @@ class RichEditorComponent {
|
|
|
39889
39928
|
}
|
|
39890
39929
|
</ion-button>
|
|
39891
39930
|
}
|
|
39931
|
+
<!-- Label trick (mismo patrón que val-attachment-uploader): el
|
|
39932
|
+
<label> nativo reenvía el clic al input como gesto confiable,
|
|
39933
|
+
ion-button + input.click() programático falla en iOS PWA. -->
|
|
39934
|
+
<label
|
|
39935
|
+
class="rich-editor__image-btn"
|
|
39936
|
+
[class.rich-editor__image-btn--disabled]="uploadingImage()"
|
|
39937
|
+
[attr.aria-label]="t('image')"
|
|
39938
|
+
>
|
|
39939
|
+
@if (uploadingImage()) {
|
|
39940
|
+
<ion-spinner name="dots" />
|
|
39941
|
+
} @else {
|
|
39942
|
+
<ion-icon name="image-outline" />
|
|
39943
|
+
}
|
|
39944
|
+
<input type="file" accept="image/*" [disabled]="uploadingImage()" (change)="onImageSelected($event)" />
|
|
39945
|
+
</label>
|
|
39892
39946
|
</div>
|
|
39893
39947
|
}
|
|
39894
39948
|
<div #editorHost class="rich-editor__surface"></div>
|
|
39895
39949
|
</div>
|
|
39896
|
-
`, isInline: true, styles: [".rich-editor{display:flex;flex-direction:column;gap:8px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:8px;overflow:hidden}.rich-editor--disabled{opacity:.7}.rich-editor__toolbar{display:flex;flex-wrap:wrap;gap:2px;padding:6px 8px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-light, #f4f5f8)}.rich-editor__toolbar ion-button{--padding-start: 8px;--padding-end: 8px;margin:0;font-size:.8125rem;min-width:32px}.rich-editor__btn--active{--background: rgba(var(--ion-color-primary-rgb), .14);--color: var(--ion-color-primary);border-radius:6px}.rich-editor__surface{padding:12px 16px;min-height:160px}.rich-editor__surface ::ng-deep .ProseMirror{outline:none;min-height:140px;color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.5}.rich-editor__surface ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--ion-color-medium, #92949c);float:left;height:0;pointer-events:none}.rich-editor__surface ::ng-deep .ProseMirror p{margin:0 0 .75em}.rich-editor__surface ::ng-deep .ProseMirror h1,.rich-editor__surface ::ng-deep .ProseMirror h2,.rich-editor__surface ::ng-deep .ProseMirror h3{margin:0 0 .5em;color:var(--ion-text-color, #000)}.rich-editor__surface ::ng-deep .ProseMirror blockquote{margin:0 0 .75em;padding-left:12px;border-left:3px solid var(--ion-color-primary, #3880ff);color:var(--ion-color-medium, #92949c)}.rich-editor__surface ::ng-deep .ProseMirror ul,.rich-editor__surface ::ng-deep .ProseMirror ol{margin:0 0 .75em;padding-left:24px}.rich-editor__surface ::ng-deep .ProseMirror img{max-width:100%;border-radius:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
|
|
39950
|
+
`, isInline: true, styles: [".rich-editor{display:flex;flex-direction:column;gap:8px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:8px;overflow:hidden}.rich-editor--disabled{opacity:.7}.rich-editor__toolbar{display:flex;flex-wrap:wrap;gap:2px;padding:6px 8px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-light, #f4f5f8)}.rich-editor__toolbar ion-button{--padding-start: 8px;--padding-end: 8px;margin:0;font-size:.8125rem;min-width:32px}.rich-editor__btn--active{--background: rgba(var(--ion-color-primary-rgb), .14);--color: var(--ion-color-primary);border-radius:6px}.rich-editor__image-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;margin:0 0 0 4px;border-radius:6px;color:var(--ion-color-dark, #232323);font-size:1.125rem;cursor:pointer}.rich-editor__image-btn:hover{background:#0000000f}.rich-editor__image-btn--disabled{opacity:.5;pointer-events:none}.rich-editor__image-btn input[type=file]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.rich-editor__surface{padding:12px 16px;min-height:160px}.rich-editor__surface ::ng-deep .ProseMirror{outline:none;min-height:140px;color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.5}.rich-editor__surface ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--ion-color-medium, #92949c);float:left;height:0;pointer-events:none}.rich-editor__surface ::ng-deep .ProseMirror p{margin:0 0 .75em}.rich-editor__surface ::ng-deep .ProseMirror h1,.rich-editor__surface ::ng-deep .ProseMirror h2,.rich-editor__surface ::ng-deep .ProseMirror h3{margin:0 0 .5em;color:var(--ion-text-color, #000)}.rich-editor__surface ::ng-deep .ProseMirror blockquote{margin:0 0 .75em;padding-left:12px;border-left:3px solid var(--ion-color-primary, #3880ff);color:var(--ion-color-medium, #92949c)}.rich-editor__surface ::ng-deep .ProseMirror ul,.rich-editor__surface ::ng-deep .ProseMirror ol{margin:0 0 .75em;padding-left:24px}.rich-editor__surface ::ng-deep .ProseMirror img{max-width:100%;border-radius:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }] }); }
|
|
39897
39951
|
}
|
|
39898
39952
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RichEditorComponent, decorators: [{
|
|
39899
39953
|
type: Component,
|
|
39900
|
-
args: [{ selector: 'val-rich-editor', standalone: true, imports: [CommonModule, IonButton, IonIcon], template: `
|
|
39954
|
+
args: [{ selector: 'val-rich-editor', standalone: true, imports: [CommonModule, IonButton, IonIcon, IonSpinner], template: `
|
|
39901
39955
|
<div class="rich-editor" [class.rich-editor--disabled]="props.disabled">
|
|
39902
39956
|
@if (!props.disabled) {
|
|
39903
39957
|
<div class="rich-editor__toolbar" role="toolbar">
|
|
@@ -39939,11 +39993,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
39939
39993
|
}
|
|
39940
39994
|
</ion-button>
|
|
39941
39995
|
}
|
|
39996
|
+
<!-- Label trick (mismo patrón que val-attachment-uploader): el
|
|
39997
|
+
<label> nativo reenvía el clic al input como gesto confiable,
|
|
39998
|
+
ion-button + input.click() programático falla en iOS PWA. -->
|
|
39999
|
+
<label
|
|
40000
|
+
class="rich-editor__image-btn"
|
|
40001
|
+
[class.rich-editor__image-btn--disabled]="uploadingImage()"
|
|
40002
|
+
[attr.aria-label]="t('image')"
|
|
40003
|
+
>
|
|
40004
|
+
@if (uploadingImage()) {
|
|
40005
|
+
<ion-spinner name="dots" />
|
|
40006
|
+
} @else {
|
|
40007
|
+
<ion-icon name="image-outline" />
|
|
40008
|
+
}
|
|
40009
|
+
<input type="file" accept="image/*" [disabled]="uploadingImage()" (change)="onImageSelected($event)" />
|
|
40010
|
+
</label>
|
|
39942
40011
|
</div>
|
|
39943
40012
|
}
|
|
39944
40013
|
<div #editorHost class="rich-editor__surface"></div>
|
|
39945
40014
|
</div>
|
|
39946
|
-
`, styles: [".rich-editor{display:flex;flex-direction:column;gap:8px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:8px;overflow:hidden}.rich-editor--disabled{opacity:.7}.rich-editor__toolbar{display:flex;flex-wrap:wrap;gap:2px;padding:6px 8px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-light, #f4f5f8)}.rich-editor__toolbar ion-button{--padding-start: 8px;--padding-end: 8px;margin:0;font-size:.8125rem;min-width:32px}.rich-editor__btn--active{--background: rgba(var(--ion-color-primary-rgb), .14);--color: var(--ion-color-primary);border-radius:6px}.rich-editor__surface{padding:12px 16px;min-height:160px}.rich-editor__surface ::ng-deep .ProseMirror{outline:none;min-height:140px;color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.5}.rich-editor__surface ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--ion-color-medium, #92949c);float:left;height:0;pointer-events:none}.rich-editor__surface ::ng-deep .ProseMirror p{margin:0 0 .75em}.rich-editor__surface ::ng-deep .ProseMirror h1,.rich-editor__surface ::ng-deep .ProseMirror h2,.rich-editor__surface ::ng-deep .ProseMirror h3{margin:0 0 .5em;color:var(--ion-text-color, #000)}.rich-editor__surface ::ng-deep .ProseMirror blockquote{margin:0 0 .75em;padding-left:12px;border-left:3px solid var(--ion-color-primary, #3880ff);color:var(--ion-color-medium, #92949c)}.rich-editor__surface ::ng-deep .ProseMirror ul,.rich-editor__surface ::ng-deep .ProseMirror ol{margin:0 0 .75em;padding-left:24px}.rich-editor__surface ::ng-deep .ProseMirror img{max-width:100%;border-radius:8px}\n"] }]
|
|
40015
|
+
`, styles: [".rich-editor{display:flex;flex-direction:column;gap:8px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:8px;overflow:hidden}.rich-editor--disabled{opacity:.7}.rich-editor__toolbar{display:flex;flex-wrap:wrap;gap:2px;padding:6px 8px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-light, #f4f5f8)}.rich-editor__toolbar ion-button{--padding-start: 8px;--padding-end: 8px;margin:0;font-size:.8125rem;min-width:32px}.rich-editor__btn--active{--background: rgba(var(--ion-color-primary-rgb), .14);--color: var(--ion-color-primary);border-radius:6px}.rich-editor__image-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;margin:0 0 0 4px;border-radius:6px;color:var(--ion-color-dark, #232323);font-size:1.125rem;cursor:pointer}.rich-editor__image-btn:hover{background:#0000000f}.rich-editor__image-btn--disabled{opacity:.5;pointer-events:none}.rich-editor__image-btn input[type=file]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.rich-editor__surface{padding:12px 16px;min-height:160px}.rich-editor__surface ::ng-deep .ProseMirror{outline:none;min-height:140px;color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.5}.rich-editor__surface ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--ion-color-medium, #92949c);float:left;height:0;pointer-events:none}.rich-editor__surface ::ng-deep .ProseMirror p{margin:0 0 .75em}.rich-editor__surface ::ng-deep .ProseMirror h1,.rich-editor__surface ::ng-deep .ProseMirror h2,.rich-editor__surface ::ng-deep .ProseMirror h3{margin:0 0 .5em;color:var(--ion-text-color, #000)}.rich-editor__surface ::ng-deep .ProseMirror blockquote{margin:0 0 .75em;padding-left:12px;border-left:3px solid var(--ion-color-primary, #3880ff);color:var(--ion-color-medium, #92949c)}.rich-editor__surface ::ng-deep .ProseMirror ul,.rich-editor__surface ::ng-deep .ProseMirror ol{margin:0 0 .75em;padding-left:24px}.rich-editor__surface ::ng-deep .ProseMirror img{max-width:100%;border-radius:8px}\n"] }]
|
|
39947
40016
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
39948
40017
|
type: Input
|
|
39949
40018
|
}], articleChange: [{
|