@innovastudio/contentbuilder 1.5.218 → 1.5.220

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/index.d.ts CHANGED
@@ -587,6 +587,12 @@ declare class ContentBuilder {
587
587
 
588
588
  generateImage(prompt: string, callback?: () => void): void;
589
589
  openFilePicker(type?: string, callback?: () => void): void;
590
+ /** Is an asset manager configured for this type of file? */
591
+ hasFilePicker(type?: string): boolean;
592
+ /** Upload a file from the user's computer; the callback gets its URL. */
593
+ openFileUpload(type?: string, callback?: (url: string) => void): void;
594
+ /** Select and upload buttons for a URL field in a plugin's settings, in the editor's own order. Select is included only where an asset manager is configured. */
595
+ createFileButtons(type?: string, callback?: (url: string) => void): HTMLButtonElement[];
590
596
  openColorPicker(
591
597
  currentColor: string,
592
598
  callback: (color: string) => void,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@innovastudio/contentbuilder",
4
- "version": "1.5.218",
4
+ "version": "1.5.220",
5
5
  "description": "",
6
6
  "main": "public/contentbuilder/contentbuilder.esm.js",
7
7
  "types": "index.d.ts",
@@ -29561,10 +29561,10 @@ class Image$1 {
29561
29561
 
29562
29562
  <div class="cb-field">
29563
29563
  <label class="label-checkbox">
29564
- <input class="input-newwindow" id="__input_newwindow2" type="checkbox" /> ${util.out('Open new window')}
29564
+ <input class="input-newwindow" id="__input_newwindow2" type="checkbox" /> ${util.out('Open in new tab')}
29565
29565
  </label>
29566
- <label class="label-checkbox" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
29567
- <input class="input-openlightbox" id="__input_openlightbox2" type="checkbox" /> ${util.out('Open in a lightbox (for image, video or Youtube)')}
29566
+ <label class="label-checkbox mt-1" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
29567
+ <input class="input-openlightbox" id="__input_openlightbox2" type="checkbox" /> ${util.out('Open in a lightbox')}
29568
29568
  </label>
29569
29569
  </div>
29570
29570
 
@@ -30644,9 +30644,22 @@ class ButtonEditor {
30644
30644
  if (cmd === 'href') setHref(el, value);else if (cmd === 'newtab') setNewTab(el, value);else if (cmd === 'fontsize') setFontSize(el, value === '' ? '' : parseInt(value, 10));else if (cmd === 'tracking') {
30645
30645
  if (value !== '') setTracking(el, parseInt(value, 10));
30646
30646
  } else if (cmd === 'margin') setMargin(el, side, value === '' ? 0 : parseInt(value, 10));
30647
+ if (cmd === 'href' || cmd === 'newtab') this.syncLinkDialog(el);
30647
30648
  this.builder.opts.onChange();
30648
30649
  this.realtime();
30649
30650
  }
30651
+
30652
+ // The link dialog edits the same href/target and can be open alongside this
30653
+ // panel. Its fields are read when it opens, so without this its Ok would
30654
+ // write back the link as it was before the panel touched it.
30655
+ syncLinkDialog(el) {
30656
+ const modal = this.builderStuff.querySelector('.is-modal.createlink');
30657
+ if (!modal || !this.dom.hasClass(modal, 'active')) return;
30658
+ const url = modal.querySelector('.input-url');
30659
+ if (url && document.activeElement !== url) url.value = el.getAttribute('href') || '';
30660
+ const newwindow = modal.querySelector('.input-newwindow');
30661
+ if (newwindow) newwindow.checked = el.getAttribute('target') === '_blank';
30662
+ }
30650
30663
  openColor(cmd, btn) {
30651
30664
  const el = this.getButton();
30652
30665
  if (!el) return;
@@ -30965,6 +30978,23 @@ class Hyperlink {
30965
30978
  this.buttonEditor = new ButtonEditor(this.builder);
30966
30979
  }
30967
30980
  }
30981
+
30982
+ // The link dialog and a button UI can be open at the same time and both
30983
+ // show the link's href and target. After the dialog writes to the link,
30984
+ // the button UI has to re-read it, or it keeps showing (and writing back)
30985
+ // the old values. The host can own that UI instead (onButtonEdit).
30986
+ syncButtonUI(link) {
30987
+ if (!link || !link.isConnected) return;
30988
+ if (this.builder.opts.onButtonEdit) {
30989
+ if (link === this.builder.activeLinkButton) this.builder.opts.onButtonEdit(link);
30990
+ return;
30991
+ }
30992
+ const dom = this.builder.dom;
30993
+ const buttonModal = this.builder.builderStuff.querySelector('.is-modal.buttoneditor');
30994
+ if (!buttonModal || !dom.hasClass(buttonModal, 'active')) return;
30995
+ if (!this.buttonEditor) this.buttonEditor = new ButtonEditor(this.builder);
30996
+ this.buttonEditor.realtime();
30997
+ }
30968
30998
  renderPanel() {
30969
30999
  const builderStuff = this.builder.builderStuff;
30970
31000
  const util = this.builder.util;
@@ -30996,10 +31026,10 @@ class Hyperlink {
30996
31026
  </div>
30997
31027
  <div class="cb-field">
30998
31028
  <label class="label-checkbox">
30999
- <input class="input-newwindow" id="__input_newwindow" type="checkbox" /> ${util.out('Open new window')}
31029
+ <input class="input-newwindow" id="__input_newwindow" type="checkbox" /> ${util.out('Open in new tab')}
31000
31030
  </label>
31001
- <label class="label-checkbox" id="lblOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
31002
- <input class="input-openlightbox" id="__input_openlightbox" type="checkbox" /> ${util.out('Open in a lightbox (for image, video or Youtube)')}
31031
+ <label class="label-checkbox mt-1" id="lblOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
31032
+ <input class="input-openlightbox" id="__input_openlightbox" type="checkbox" /> ${util.out('Open in a lightbox')}
31003
31033
  </label>
31004
31034
  </div>
31005
31035
  <div class="cb-field">
@@ -31042,6 +31072,7 @@ class Hyperlink {
31042
31072
  let inputOk = modal.querySelector('.input-ok');
31043
31073
  dom.addEventListener(inputOk, 'click', () => {
31044
31074
  this.builder.uo.saveForUndo();
31075
+ let editedLink = null;
31045
31076
  let link = this.builder.activeLink;
31046
31077
  if (link) {
31047
31078
  // Edit Existing Link
@@ -31051,35 +31082,19 @@ class Hyperlink {
31051
31082
  let linktext = modal.querySelector('.input-text').value;
31052
31083
  if (linktext === '') linktext = url;
31053
31084
 
31054
- // Engine seam: edit / unlink an existing link via Inscribe.
31055
- // A cleared URL means UNLINK. Icon links stay legacy.
31056
- if (this.builder.opts.engine === 'inscribe' && !this.builder.activeIcon) {
31085
+ // Engine seam: only UNLINK (a cleared URL) goes through
31086
+ // Inscribe. An edit is applied in place below: region.link()
31087
+ // re-wraps the selection in a NEW plain <a>, which dropped
31088
+ // the link's classes and inline styles, so changing a
31089
+ // button's URL stripped the button. Icon links stay legacy.
31090
+ if (url === '' && this.builder.opts.engine === 'inscribe' && !this.builder.activeIcon) {
31057
31091
  const region = this.builder.editingEngine.active;
31058
31092
  if (region && link.isConnected) {
31059
31093
  dom.selectElementContents(link); // target the link
31060
- if (url !== '') {
31061
- const attrs = {
31062
- target: modal.querySelector('.input-newwindow').checked ? '_blank' : '',
31063
- title: title || '',
31064
- class: modal.querySelector('.input-openlightbox').checked ? 'is-lightbox' : ''
31065
- };
31066
- const created = region.link(url, attrs); // re-wraps, returns <a>
31067
- if (created && linktext && created.textContent !== linktext) {
31068
- created.textContent = linktext;
31069
- }
31070
- this.builder.opts.onChange();
31071
- util.hideModal(modal);
31072
- if (created && created.isConnected) {
31073
- region.focus();
31074
- dom.selectElementContents(created);
31075
- util.saveSelection();
31076
- }
31077
- } else {
31078
- region.unlink();
31079
- this.hideTool();
31080
- this.builder.opts.onChange();
31081
- util.hideModal(modal);
31082
- }
31094
+ region.unlink();
31095
+ this.hideTool();
31096
+ this.builder.opts.onChange();
31097
+ util.hideModal(modal);
31083
31098
  return;
31084
31099
  }
31085
31100
  }
@@ -31087,6 +31102,7 @@ class Hyperlink {
31087
31102
  link.setAttribute('href', url);
31088
31103
  if (modal.querySelector('.input-newwindow').checked) {
31089
31104
  link.setAttribute('target', '_blank');
31105
+ if (!link.getAttribute('rel')) link.setAttribute('rel', 'noopener noreferrer');
31090
31106
  } else {
31091
31107
  link.removeAttribute('target');
31092
31108
  }
@@ -31098,7 +31114,14 @@ class Hyperlink {
31098
31114
  if (this.builder.activeIcon) ; else {
31099
31115
  link.innerHTML = linktext;
31100
31116
  }
31101
- link.setAttribute('title', title);
31117
+ if (title) link.setAttribute('title', title);else link.removeAttribute('title');
31118
+
31119
+ // A button UI (this editor's Button panel, or the
31120
+ // host's own) shows the same link and can be open at
31121
+ // the same time as this dialog, so it has to re-read
31122
+ // what was just written.
31123
+ this.syncButtonUI(link);
31124
+ editedLink = link;
31102
31125
  } else {
31103
31126
  // link.outerHTML = link.innerHTML;
31104
31127
  this.hideTool();
@@ -31256,6 +31279,17 @@ class Hyperlink {
31256
31279
  this.builder.opts.onRender();
31257
31280
  }
31258
31281
  util.hideModal(modal);
31282
+
31283
+ // hideModal() blurs, so the caret goes back on the edited link
31284
+ // afterwards: the dialog can be reopened straight away.
31285
+ if (editedLink && editedLink.isConnected && !this.builder.activeIcon) {
31286
+ const region = this.builder.editingEngine.active;
31287
+ if (region) {
31288
+ region.focus();
31289
+ dom.selectElementContents(editedLink);
31290
+ util.saveSelection();
31291
+ }
31292
+ }
31259
31293
  });
31260
31294
  let inputCancel = modal.querySelector('.input-cancel');
31261
31295
  dom.addEventListener(inputCancel, 'click', () => {
@@ -31401,7 +31435,10 @@ class Hyperlink {
31401
31435
  }
31402
31436
 
31403
31437
  // Button Stuff
31404
- if (this.builder.useButtonPlugin) {
31438
+ // A host with its own button UI (onButtonEdit) already opens it by
31439
+ // selecting the button, so the pencil is a second route to the same
31440
+ // panel and is left out.
31441
+ if (this.builder.useButtonPlugin || this.builder.opts.onButtonEdit) {
31405
31442
  linkTool.querySelector('.link-button-edit').style.display = 'none';
31406
31443
  } else {
31407
31444
  if (link && this.builder.activeLinkButton) linkTool.querySelector('.link-button-edit').style.display = 'block';else linkTool.querySelector('.link-button-edit').style.display = 'none';
@@ -31705,6 +31742,11 @@ class Button {
31705
31742
  this.buttonEditor.realtime();
31706
31743
  }
31707
31744
  const buttonTool = this.buttonTool;
31745
+
31746
+ // A host with its own button UI (onButtonEdit) already opens it by
31747
+ // selecting the button, so the pencil is a second route to the same
31748
+ // panel and is left out.
31749
+ buttonTool.querySelector('.button-edit').style.display = this.builder.opts.onButtonEdit ? 'none' : '';
31708
31750
  let top = activeButton.getBoundingClientRect().top + this.builder.win.pageYOffset;
31709
31751
  let left = activeButton.getBoundingClientRect().left;
31710
31752
  buttonTool.style.display = 'flex';
@@ -46273,7 +46315,16 @@ class ColorPickerKelir {
46273
46315
  }
46274
46316
 
46275
46317
  // Close the picker on scroll — it's positioned once, so scrolling detaches it.
46276
- const hideOnScroll = () => {
46318
+ //
46319
+ // The page moving under the picker is the only scroll that counts. A text
46320
+ // input scrolls its own content whenever the caret passes the edge — on
46321
+ // paste, on Delete, on Cmd+Left/Right — and that is a scroll event too,
46322
+ // which a capture listener on window receives even though it does not
46323
+ // bubble. Without this guard the picker closed while its own hex field
46324
+ // was being edited.
46325
+ const hideOnScroll = e => {
46326
+ const target = e && e.target;
46327
+ if (target && target.nodeType === 1 && popPicker.contains(target)) return;
46277
46328
  this.builder.util.hidePop(popPicker);
46278
46329
  window.removeEventListener('scroll', hideOnScroll, true);
46279
46330
  if (this.builder.win && this.builder.win !== window) this.builder.win.removeEventListener('scroll', hideOnScroll, true);
@@ -92847,6 +92898,90 @@ class ContentBuilder {
92847
92898
  openFilePicker(type, callback) {
92848
92899
  this.openAssetSelect(type, callback);
92849
92900
  }
92901
+
92902
+ /**
92903
+ * Is an asset manager reachable for this type of file? Mirrors the branches
92904
+ * openAsset() takes: either a picker page (filePicker / imageSelect / ...)
92905
+ * or the host's own onXSelectClick handler.
92906
+ */
92907
+ hasFilePicker(type) {
92908
+ if (type === 'media') return !!(this.onMediaSelectClick || this.onImageSelectClick || this.mediaSelect || this.imageSelect);
92909
+ if (type === 'video') return !!(this.onVideoSelectClick || this.videoSelect);
92910
+ if (type === 'audio') return !!(this.onAudioSelectClick || this.audioSelect);
92911
+ if (type === 'all') return !!(this.onFileSelectClick || this.fileSelect);
92912
+ return !!(this.onImageSelectClick || this.imageSelect);
92913
+ }
92914
+
92915
+ /**
92916
+ * Upload a file from the user's computer and hand its URL to `callback`.
92917
+ *
92918
+ * The per-type upload handler always exists — a builder configured without
92919
+ * one gets the data-URL fallback — so this is available whatever the host
92920
+ * set up. The URL comes back through returnUrl(), the same path the image
92921
+ * tool's upload button uses.
92922
+ */
92923
+ openFileUpload(type, callback) {
92924
+ const accept = type === 'video' ? 'video/*' : type === 'audio' ? 'audio/*' : type === 'media' ? 'image/*,video/*' : type === 'all' ? '*' : 'image/*';
92925
+ const handler = type === 'video' ? this.onVideoUpload : type === 'audio' ? this.onAudioUpload : type === 'media' ? this.onMediaUpload : type === 'all' ? this.onFileUpload : this.onImageUpload;
92926
+ if (!handler) return;
92927
+ const inpFile = document.createElement('input');
92928
+ inpFile.type = 'file';
92929
+ inpFile.accept = accept;
92930
+ inpFile.style.display = 'none';
92931
+ document.body.appendChild(inpFile);
92932
+ inpFile.addEventListener('change', async e => {
92933
+ if (!e.target.files || !e.target.files.length) {
92934
+ inpFile.remove();
92935
+ return;
92936
+ }
92937
+ this.onAssetUpload = url => {
92938
+ if (callback) callback(url);
92939
+ };
92940
+ await handler(e);
92941
+ inpFile.remove();
92942
+ });
92943
+ inpFile.click();
92944
+ }
92945
+
92946
+ /**
92947
+ * The file controls for a URL field in a plugin's settings: select from the
92948
+ * asset manager, and upload from the computer.
92949
+ *
92950
+ * Returns the buttons to append next to the input, in the order and with the
92951
+ * icons the editor's own image dialog uses — select first, upload last, and
92952
+ * select only where an asset manager is configured. A plugin appends what it
92953
+ * gets rather than deciding, so a builder without an asset manager never
92954
+ * shows a button that cannot open one.
92955
+ *
92956
+ * const [ ...buttons ] = builder.createFileButtons('media', (url) => {...});
92957
+ * row.append(input, ...buttons);
92958
+ *
92959
+ * `type` may be a function for a field whose kind follows another control
92960
+ * (a Media Type select, say) — it is then read on each click.
92961
+ */
92962
+ createFileButtons(type, callback) {
92963
+ const out = s => this.util.out(s);
92964
+ const fileType = () => typeof type === 'function' ? type() : type;
92965
+ const button = (label, icon, onClick) => {
92966
+ const btn = document.createElement('button');
92967
+ btn.type = 'button';
92968
+ btn.className = 'cbx-iconbtn';
92969
+ btn.title = label;
92970
+ btn.setAttribute('aria-label', label);
92971
+ btn.innerHTML = `<svg aria-hidden="true"><use xlink:href="#${icon}"></use></svg>`;
92972
+ btn.addEventListener('click', e => {
92973
+ e.preventDefault();
92974
+ onClick(btn);
92975
+ });
92976
+ return btn;
92977
+ };
92978
+ const buttons = [];
92979
+ if (this.hasFilePicker(fileType())) {
92980
+ buttons.push(button(out('Select'), 'icon-folder', btn => this.openFilePicker(fileType(), callback, btn)));
92981
+ }
92982
+ buttons.push(button(out('Upload'), 'icon-upload', () => this.openFileUpload(fileType(), callback)));
92983
+ return buttons;
92984
+ }
92850
92985
  openAssetSelect(targetAssetType, callback, defaultValue) {
92851
92986
  const inpUrl = document.createElement('input');
92852
92987