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.
@@ -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.972';
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: [{