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;