diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js
index 3fd7a7079..3e5b63c34 100644
--- a/abstract/UploaderBlock.js
+++ b/abstract/UploaderBlock.js
@@ -17,6 +17,7 @@ import { uploaderBlockCtx } from './CTX.js';
import { EVENT_TYPES, EventData, EventManager } from './EventManager.js';
import { TypedCollection } from './TypedCollection.js';
import { uploadEntrySchema } from './uploadEntrySchema.js';
+import { serializeCsv } from '../blocks/utils/comma-separated.js';
export class UploaderBlock extends ActivityBlock {
couldBeUploadCollectionOwner = false;
@@ -204,7 +205,7 @@ export class UploaderBlock extends ActivityBlock {
/** @param {{ captureCamera?: boolean }} options */
openSystemDialog(options = {}) {
- let accept = mergeFileTypes([this.cfg.accept ?? '', ...(this.cfg.imgOnly ? IMAGE_ACCEPT_LIST : [])]).join(',');
+ let accept = serializeCsv(mergeFileTypes([this.cfg.accept ?? '', ...(this.cfg.imgOnly ? IMAGE_ACCEPT_LIST : [])]));
if (this.cfg.accept && !!this.cfg.imgOnly) {
console.warn(
@@ -218,7 +219,7 @@ export class UploaderBlock extends ActivityBlock {
this.fileInput.multiple = this.cfg.multiple;
if (options.captureCamera) {
this.fileInput.capture = '';
- this.fileInput.accept = IMAGE_ACCEPT_LIST.join(',');
+ this.fileInput.accept = serializeCsv(IMAGE_ACCEPT_LIST);
} else {
this.fileInput.accept = accept;
}
diff --git a/blocks/CloudImageEditor/src/CloudImageEditorBlock.js b/blocks/CloudImageEditor/src/CloudImageEditorBlock.js
index 60247e2df..50e398b88 100644
--- a/blocks/CloudImageEditor/src/CloudImageEditorBlock.js
+++ b/blocks/CloudImageEditor/src/CloudImageEditorBlock.js
@@ -12,6 +12,7 @@ import { CloudImageEditorBase } from './CloudImageEditorBase.js';
import { classNames } from './lib/classNames.js';
import { parseCropPreset } from './lib/parseCropPreset.js';
import { operationsToTransformations, transformationsToOperations } from './lib/transformationUtils.js';
+import { parseTabs } from './lib/parseTabs.js';
import { initState } from './state.js';
import { TEMPLATE } from './template.js';
import { TabId } from './toolbar-constants.js';
@@ -159,6 +160,13 @@ export class CloudImageEditorBlock extends CloudImageEditorBase {
this.$['*cropPresetList'] = parseCropPreset(val);
});
+ this.sub(
+ 'tabs',
+ /** @param {string} val */ (val) => {
+ this.$['*tabList'] = parseTabs(val);
+ }
+ );
+
this.sub('*tabId', (tabId) => {
this.ref['img-el'].className = classNames('image', {
image_hidden_to_cropper: tabId === TabId.CROP,
@@ -211,4 +219,5 @@ CloudImageEditorBlock.bindAttributes({
uuid: 'uuid',
'cdn-url': 'cdnUrl',
'crop-preset': 'cropPreset',
+ tabs: 'tabs',
});
diff --git a/blocks/CloudImageEditor/src/EditorToolbar.js b/blocks/CloudImageEditor/src/EditorToolbar.js
index b060eb205..6080d0acc 100644
--- a/blocks/CloudImageEditor/src/EditorToolbar.js
+++ b/blocks/CloudImageEditor/src/EditorToolbar.js
@@ -1,3 +1,4 @@
+// @ts-check
import { debounce } from '../../utils/debounce.js';
import { CloudImageEditorBase } from './CloudImageEditorBase.js';
import { EditorCropButtonControl } from './EditorCropButtonControl.js';
@@ -9,24 +10,26 @@ import {
ALL_COLOR_OPERATIONS,
ALL_CROP_OPERATIONS,
ALL_FILTERS,
+ ALL_TABS,
COLOR_OPERATIONS_CONFIG,
TabId,
- TABS,
} from './toolbar-constants.js';
import { viewerImageSrc } from './util.js';
/** @param {String} id */
function renderTabToggle(id) {
return /* HTML */ `
-
-
+
+
+
+
`;
}
@@ -67,9 +70,13 @@ export class EditorToolbar extends CloudImageEditorBase {
'presence.mainToolbar': true,
'presence.subToolbar': false,
+ 'presence.tabToggles': true,
'presence.tabContent.crop': false,
- 'presence.tabContent.sliders': false,
+ 'presence.tabContent.tuning': false,
'presence.tabContent.filters': false,
+ 'presence.tabToggle.crop': true,
+ 'presence.tabToggle.tuning': true,
+ 'presence.tabToggle.filters': true,
'presence.subTopToolbarStyles': {
hidden: 'sub-toolbar--top-hidden',
visible: 'sub-toolbar--visible',
@@ -82,24 +89,35 @@ export class EditorToolbar extends CloudImageEditorBase {
hidden: 'tab-content--hidden',
visible: 'tab-content--visible',
},
- 'on.cancel': (e) => {
+ 'presence.tabToggleStyles': {
+ hidden: 'tab-toggle--hidden',
+ visible: 'tab-toggle--visible',
+ },
+ 'presence.tabTogglesStyles': {
+ hidden: 'tab-toggles--hidden',
+ visible: 'tab-toggles--visible',
+ },
+ 'on.cancel': () => {
this._cancelPreload && this._cancelPreload();
this.$['*on.cancel']();
},
- 'on.apply': (e) => {
+ 'on.apply': () => {
this.$['*on.apply'](this.$['*editorTransformations']);
},
- 'on.applySlider': (e) => {
+ 'on.applySlider': () => {
this.ref['slider-el'].apply();
this._onSliderClose();
},
- 'on.cancelSlider': (e) => {
+ 'on.cancelSlider': () => {
this.ref['slider-el'].cancel();
this._onSliderClose();
},
+ /** @param {MouseEvent} e */
'on.clickTab': (e) => {
- let id = e.currentTarget.getAttribute('data-id');
- this._activateTab(id, { fromViewer: false });
+ const id = /** @type {HTMLElement} */ (e.currentTarget).getAttribute('data-id');
+ if (id) {
+ this._activateTab(id, { fromViewer: false });
+ }
},
};
@@ -111,7 +129,7 @@ export class EditorToolbar extends CloudImageEditorBase {
/** @private */
_onSliderClose() {
this.$['*showSlider'] = false;
- if (this.$['*tabId'] === TabId.SLIDERS) {
+ if (this.$['*tabId'] === TabId.TUNING) {
this.ref['tooltip-el'].classList.toggle('info-tooltip_visible', false);
}
}
@@ -121,8 +139,9 @@ export class EditorToolbar extends CloudImageEditorBase {
* @param {String} operation
*/
_createOperationControl(operation) {
- let el = EditorOperationControl.is && new EditorOperationControl();
- el['operation'] = operation;
+ let el = new EditorOperationControl();
+ // @ts-expect-error TODO: fix
+ el.operation = operation;
return el;
}
@@ -131,8 +150,9 @@ export class EditorToolbar extends CloudImageEditorBase {
* @param {String} filter
*/
_createFilterControl(filter) {
- let el = EditorFilterControl.is && new EditorFilterControl();
- el['filter'] = filter;
+ let el = new EditorFilterControl();
+ // @ts-expect-error TODO: fix
+ el.filter = filter;
return el;
}
@@ -141,8 +161,9 @@ export class EditorToolbar extends CloudImageEditorBase {
* @param {String} operation
*/
_createToggleControl(operation) {
- let el = EditorCropButtonControl.is && new EditorCropButtonControl();
- el['operation'] = operation;
+ let el = new EditorCropButtonControl();
+ // @ts-expect-error TODO: fix
+ el.operation = operation;
return el;
}
@@ -155,26 +176,30 @@ export class EditorToolbar extends CloudImageEditorBase {
let fr = document.createDocumentFragment();
if (tabId === TabId.CROP) {
- this.$.cropOperations.forEach((operation) => {
- let el = this._createToggleControl(operation);
- // @ts-ignore
- fr.appendChild(el);
- });
+ this.$.cropOperations.forEach(
+ /** @param {string} operation */ (operation) => {
+ let el = this._createToggleControl(operation);
+ // @ts-ignore
+ fr.appendChild(el);
+ }
+ );
} else if (tabId === TabId.FILTERS) {
[FAKE_ORIGINAL_FILTER, ...this.$.filters].forEach((filterId) => {
let el = this._createFilterControl(filterId);
// @ts-ignore
fr.appendChild(el);
});
- } else if (tabId === TabId.SLIDERS) {
- this.$.colorOperations.forEach((operation) => {
- let el = this._createOperationControl(operation);
- // @ts-ignore
- fr.appendChild(el);
- });
+ } else if (tabId === TabId.TUNING) {
+ this.$.colorOperations.forEach(
+ /** @param {string} operation */ (operation) => {
+ let el = this._createOperationControl(operation);
+ // @ts-ignore
+ fr.appendChild(el);
+ }
+ );
}
- fr.childNodes.forEach((/** @type {HTMLElement} */ el, idx) => {
+ [...fr.children].forEach((el, idx) => {
if (idx === fr.childNodes.length - 1) {
el.classList.add('controls-list_last-item');
}
@@ -200,7 +225,7 @@ export class EditorToolbar extends CloudImageEditorBase {
this.$['*cropperEl'].deactivate();
}
- for (let tabId of TABS) {
+ for (let tabId of ALL_TABS) {
let isCurrentTab = tabId === id;
let tabToggleEl = this.ref[`tab-toggle-${tabId}`];
@@ -248,13 +273,18 @@ export class EditorToolbar extends CloudImageEditorBase {
}
}
- /** @private */
+ /**
+ * @private
+ * @param {boolean} show
+ */
_showLoader(show) {
this.$.showLoader = show;
}
_updateInfoTooltip = debounce(() => {
- let transformations = this.$['*editorTransformations'];
+ const transformations = this.$['*editorTransformations'];
+ /** @type {keyof COLOR_OPERATIONS_CONFIG} */
+ const currentOperation = this.$['*currentOperation'];
let text = '';
let visible = false;
@@ -266,11 +296,10 @@ export class EditorToolbar extends CloudImageEditorBase {
} else {
text = this.l10n(FAKE_ORIGINAL_FILTER);
}
- } else if (this.$['*tabId'] === TabId.SLIDERS && this.$['*currentOperation']) {
+ } else if (this.$['*tabId'] === TabId.TUNING && currentOperation) {
visible = true;
- let value =
- transformations?.[this.$['*currentOperation']] || COLOR_OPERATIONS_CONFIG[this.$['*currentOperation']].zero;
- text = this.$['*currentOperation'] + ' ' + value;
+ let value = transformations?.[currentOperation] || COLOR_OPERATIONS_CONFIG[currentOperation].zero;
+ text = currentOperation + ' ' + value;
}
if (visible) {
this.$['*operationTooltip'] = text;
@@ -310,7 +339,7 @@ export class EditorToolbar extends CloudImageEditorBase {
this._updateInfoTooltip();
});
- this.sub('*originalUrl', (originalUrl) => {
+ this.sub('*originalUrl', () => {
this.$['*faderEl'] && this.$['*faderEl'].deactivate();
});
@@ -342,6 +371,16 @@ export class EditorToolbar extends CloudImageEditorBase {
this.$['presence.mainToolbar'] = !showSlider;
});
+ this.sub('*tabList', (tabList) => {
+ this.$['presence.tabToggles'] = tabList.length > 1;
+ this.$['*tabId'] = tabList[0];
+ for (const tabId of ALL_TABS) {
+ this.$[`presence.tabToggle.${tabId}`] = tabList.includes(tabId);
+ const toggleEl = this.ref[`tab-toggle-${tabId}`];
+ toggleEl.style.gridColumn = tabList.indexOf(tabId) + 1;
+ }
+ });
+
this._updateInfoTooltip();
}
}
@@ -355,13 +394,13 @@ EditorToolbar.template = /* HTML */ `
- ${TABS.map(renderTabContent).join('')}
+ ${ALL_TABS.map(renderTabContent).join('')}
-
+
- ${TABS.map(renderTabToggle).join('')}
-
+ ${ALL_TABS.map(renderTabToggle).join('')}
+
diff --git a/blocks/CloudImageEditor/src/css/common.css b/blocks/CloudImageEditor/src/css/common.css
index ffcbb6ef0..0f102d835 100644
--- a/blocks/CloudImageEditor/src/css/common.css
+++ b/blocks/CloudImageEditor/src/css/common.css
@@ -697,6 +697,18 @@ lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-
pointer-events: none;
}
+lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles > .tab-toggle.tab-toggle--visible {
+ display: contents;
+}
+
+lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles > .tab-toggle.tab-toggle--hidden {
+ display: none;
+}
+
+lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles.tab-toggles--hidden {
+ display: none;
+}
+
lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-content .controls-list_align {
display: grid;
grid-template-areas: '. inner .';
diff --git a/blocks/CloudImageEditor/src/css/icons.css b/blocks/CloudImageEditor/src/css/icons.css
index 2faea831e..1647abcab 100644
--- a/blocks/CloudImageEditor/src/css/icons.css
+++ b/blocks/CloudImageEditor/src/css/icons.css
@@ -30,7 +30,7 @@
--icon-sad: 'M2 17c4.41828-4 11.5817-4 16 0M16.5 5c0 .55228-.4477 1-1 1s-1-.44772-1-1 .4477-1 1-1 1 .44772 1 1zm-11 0c0 .55228-.44772 1-1 1s-1-.44772-1-1 .44772-1 1-1 1 .44772 1 1z';
--icon-closeMax: 'M3 3l14 14m0-14L3 17';
--icon-crop: 'M20 14H7.00513C6.45001 14 6 13.55 6 12.9949V0M0 6h13.0667c.5154 0 .9333.41787.9333.93333V20M14.5.399902L13 1.9999l1.5 1.6M13 2h2c1.6569 0 3 1.34315 3 3v2M5.5 19.5999l1.5-1.6-1.5-1.6M7 18H5c-1.65685 0-3-1.3431-3-3v-2';
- --icon-sliders: 'M8 10h11M1 10h4M1 4.5h11m3 0h4m-18 11h11m3 0h4M12 4.5a1.5 1.5 0 103 0 1.5 1.5 0 10-3 0M5 10a1.5 1.5 0 103 0 1.5 1.5 0 10-3 0M12 15.5a1.5 1.5 0 103 0 1.5 1.5 0 10-3 0';
+ --icon-tuning: 'M8 10h11M1 10h4M1 4.5h11m3 0h4m-18 11h11m3 0h4M12 4.5a1.5 1.5 0 103 0 1.5 1.5 0 10-3 0M5 10a1.5 1.5 0 103 0 1.5 1.5 0 10-3 0M12 15.5a1.5 1.5 0 103 0 1.5 1.5 0 10-3 0';
--icon-filters: 'M4.5 6.5a5.5 5.5 0 1011 0 5.5 5.5 0 10-11 0m-3.5 6a5.5 5.5 0 1011 0 5.5 5.5 0 10-11 0m7 0a5.5 5.5 0 1011 0 5.5 5.5 0 10-11 0';
--icon-done: 'M1 10.6316l5.68421 5.6842L19 4';
--icon-original: 'M0 40L40-.00000133';
diff --git a/blocks/CloudImageEditor/src/lib/parseTabs.js b/blocks/CloudImageEditor/src/lib/parseTabs.js
new file mode 100644
index 000000000..e9b1a33d0
--- /dev/null
+++ b/blocks/CloudImageEditor/src/lib/parseTabs.js
@@ -0,0 +1,14 @@
+// @ts-check
+
+import { deserealizeCsv } from '../../../utils/comma-separated.js';
+import { ALL_TABS } from '../toolbar-constants.js';
+
+/** @param {string} tabs */
+export const parseTabs = (tabs) => {
+ if (!tabs) return ALL_TABS;
+ const tabList = deserealizeCsv(tabs).filter((tab) => ALL_TABS.includes(tab));
+ if (tabList.length === 0) {
+ return ALL_TABS;
+ }
+ return tabList;
+};
diff --git a/blocks/CloudImageEditor/src/state.js b/blocks/CloudImageEditor/src/state.js
index 1a029ab55..63e443943 100644
--- a/blocks/CloudImageEditor/src/state.js
+++ b/blocks/CloudImageEditor/src/state.js
@@ -2,7 +2,9 @@
import { createCdnUrl, createCdnUrlModifiers } from '../../../utils/cdn-utils.js';
import { TRANSPARENT_PIXEL_SRC } from '../../../utils/transparentPixelSrc.js';
+import { serializeCsv } from '../../utils/comma-separated.js';
import { transformationsToOperations } from './lib/transformationUtils.js';
+import { ALL_TABS } from './toolbar-constants.js';
/** @param {import('./CloudImageEditorBlock.js').CloudImageEditorBlock} fnCtx */
export function initState(fnCtx) {
@@ -18,6 +20,7 @@ export function initState(fnCtx) {
'*editorTransformations': {},
/** @type {import('./types.js').CropPresetList} */
'*cropPresetList': [],
+ '*tabList': ALL_TABS,
entry: null,
extension: null,
@@ -33,6 +36,7 @@ export function initState(fnCtx) {
uuid: null,
cdnUrl: null,
cropPreset: '',
+ tabs: serializeCsv(ALL_TABS),
'presence.networkProblems': false,
'presence.modalCaption': true,
diff --git a/blocks/CloudImageEditor/src/toolbar-constants.js b/blocks/CloudImageEditor/src/toolbar-constants.js
index 39cd1d74a..fcc94dd50 100644
--- a/blocks/CloudImageEditor/src/toolbar-constants.js
+++ b/blocks/CloudImageEditor/src/toolbar-constants.js
@@ -1,12 +1,12 @@
+// @ts-check
import { OPERATIONS_ZEROS } from './lib/transformationUtils.js';
-/** @type {{ CROP: 'crop'; SLIDERS: 'sliders'; FILTERS: 'filters' }} */
-export const TabId = {
+export const TabId = Object.freeze({
CROP: 'crop',
- SLIDERS: 'sliders',
+ TUNING: 'tuning',
FILTERS: 'filters',
-};
-export const TABS = [TabId.CROP, TabId.SLIDERS, TabId.FILTERS];
+});
+export const ALL_TABS = [TabId.CROP, TabId.TUNING, TabId.FILTERS];
export const ALL_COLOR_OPERATIONS = [
'brightness',
@@ -65,7 +65,7 @@ export const ALL_FILTERS = [
export const ALL_CROP_OPERATIONS = ['rotate', 'mirror', 'flip'];
/** KeypointsNumber is the number of keypoints loaded from each side of zero, not total number */
-export const COLOR_OPERATIONS_CONFIG = {
+export const COLOR_OPERATIONS_CONFIG = Object.freeze({
brightness: {
zero: OPERATIONS_ZEROS.brightness,
range: [-100, 100],
@@ -111,4 +111,4 @@ export const COLOR_OPERATIONS_CONFIG = {
range: [0, 100],
keypointsNumber: 1,
},
-};
+});
diff --git a/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js b/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js
index 16bc8c518..14bf0433b 100644
--- a/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js
+++ b/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js
@@ -50,6 +50,7 @@ export class CloudImageEditorActivity extends UploaderBlock {
const instance = new CloudImageEditorBlock();
const cdnUrl = this.$.cdnUrl;
const cropPreset = this.cfg.cropPreset;
+ const tabs = this.cfg.cloudImageEditorTabs;
instance.setAttribute('ctx-name', this.ctxName);
instance.setAttribute('cdn-url', cdnUrl);
@@ -57,6 +58,9 @@ export class CloudImageEditorActivity extends UploaderBlock {
if (cropPreset) {
instance.setAttribute('crop-preset', cropPreset);
}
+ if (tabs) {
+ instance.setAttribute('tabs', tabs);
+ }
instance.addEventListener('apply', (result) => this.handleApply(result));
instance.addEventListener('cancel', () => this.handleCancel());
diff --git a/blocks/Config/initialConfig.js b/blocks/Config/initialConfig.js
index 3ccb80dee..1631d956c 100644
--- a/blocks/Config/initialConfig.js
+++ b/blocks/Config/initialConfig.js
@@ -1,11 +1,14 @@
// @ts-check
+import { ALL_TABS } from '../CloudImageEditor/src/toolbar-constants.js';
+import { serializeCsv } from '../utils/comma-separated.js';
+
export const DEFAULT_CDN_CNAME = 'https://ucarecdn.com';
export const DEFAULT_BASE_URL = 'https://upload.uploadcare.com';
export const DEFAULT_SOCIAN_BASE_URL = 'https://social.uploadcare.com';
/** @type {import('../../types/exported').ConfigType} */
-export const initialConfig = Object.freeze({
+export const initialConfig = {
pubkey: '',
multiple: true,
multipleMin: 0,
@@ -18,6 +21,7 @@ export const initialConfig = Object.freeze({
store: 'auto',
cameraMirror: false,
sourceList: 'local, url, camera, dropbox, gdrive',
+ cloudImageEditorTabs: serializeCsv(ALL_TABS),
maxLocalFileSizeBytes: 0,
thumbSize: 76,
showEmptyList: false,
@@ -51,4 +55,4 @@ export const initialConfig = Object.freeze({
userAgentIntegration: '',
metadata: null,
-});
+};
diff --git a/blocks/Config/normalizeConfigValue.js b/blocks/Config/normalizeConfigValue.js
index c84a9bcc5..d91540b30 100644
--- a/blocks/Config/normalizeConfigValue.js
+++ b/blocks/Config/normalizeConfigValue.js
@@ -41,6 +41,7 @@ const mapping = {
showEmptyList: asBoolean,
useLocalImageEditor: asBoolean,
useCloudImageEditor: asBoolean,
+ cloudImageEditorTabs: asString,
removeCopyright: asBoolean,
cropPreset: asString,
diff --git a/blocks/test/cloud-image-editor.htm b/blocks/test/cloud-image-editor.htm
index 99200bc0a..223a7edeb 100644
--- a/blocks/test/cloud-image-editor.htm
+++ b/blocks/test/cloud-image-editor.htm
@@ -27,5 +27,5 @@
-
+
diff --git a/blocks/test/raw-regular.htm b/blocks/test/raw-regular.htm
index dd3dd5d90..12057f945 100644
--- a/blocks/test/raw-regular.htm
+++ b/blocks/test/raw-regular.htm
@@ -30,5 +30,5 @@
-
+
\ No newline at end of file
diff --git a/blocks/utils/comma-separated.js b/blocks/utils/comma-separated.js
new file mode 100644
index 000000000..650468026
--- /dev/null
+++ b/blocks/utils/comma-separated.js
@@ -0,0 +1,19 @@
+// @ts-check
+
+/** @param {string} value */
+export const deserealizeCsv = (value) => {
+ if (!value) {
+ return [];
+ }
+
+ return value.split(',').map((item) => item.trim());
+};
+
+/** @param {unknown[]} value */
+export const serializeCsv = (value) => {
+ if (!value) {
+ return '';
+ }
+
+ return value.join(',');
+};
diff --git a/types/exported.d.ts b/types/exported.d.ts
index 64107e4a6..ffb34c76f 100644
--- a/types/exported.d.ts
+++ b/types/exported.d.ts
@@ -17,6 +17,7 @@ export type ConfigType = {
showEmptyList: boolean;
useLocalImageEditor: boolean;
useCloudImageEditor: boolean;
+ cloudImageEditorTabs: string;
removeCopyright: boolean;
cropPreset: string;
modalScrollLock: boolean;