diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs index 99ff09b8f..497259778 100644 --- a/.stylelintrc.cjs +++ b/.stylelintrc.cjs @@ -40,11 +40,6 @@ module.exports = { files: ['blocks/**/*.css', 'solutions/**/*.css'], ignoreFiles: ['**/test/**/*.css'], plugins: ['./stylelint-force-app-name-prefix.cjs'], - rules: { - 'plugin/stylelint-force-app-name-prefix': { - appName: 'lr', - }, - }, }, ], }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aafccbb5..a6de96aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,81 @@ -# [0.43.0](https://github.com/uploadcare/blocks/compare/v0.42.1...v0.43.0) (2024-06-14) +# [0.49.0](https://github.com/uploadcare/blocks/compare/v0.48.1...v0.49.0) (2024-07-03) + +### BEAKING CHANGES + +- **styles:** Added the `uc-` prefix to all class selectors ([#683](https://github.com/uploadcare/blocks/issues/683)) ([a3929d5](https://github.com/uploadcare/blocks/commit/a3929d599e23e221b2fe88b2d2489a35c8ee7899)). + +See the [migration guide](https://uploadcare.com/docs/file-uploader/migration-to-0.49.0/) for details. + +## [0.48.1](https://github.com/uploadcare/blocks/compare/v0.48.0...v0.48.1) (2024-07-02) + +### Bug Fixes + +- **a11y:** Added a type for the button ([#695](https://github.com/uploadcare/blocks/issues/695)) ([af1d6b6](https://github.com/uploadcare/blocks/commit/af1d6b63112cc383bcad52d099d84460da937ffa)) + +## [0.48.0](https://github.com/uploadcare/blocks/compare/v0.47.0...v0.48.0) (2024-07-02) + +# [0.47.0](https://github.com/uploadcare/blocks/compare/v0.46.3...v0.47.0) (2024-07-02) + +### BEAKING CHANGES + +- Extract public api to the composition class at the Uploader API instance that you can get using `getAPI()` method of the `lr-upload-ctx-provider` block. ([#676](https://github.com/uploadcare/blocks/issues/676)) ([ea29dd2](https://github.com/uploadcare/blocks/commit/ea29dd2ebfc5b5897c7db8f6dbda608604eea86c)). + +See the [migration guide](https://uploadcare.com/docs/file-uploader/migration-to-0.47.0/) for details. + +## [0.46.3](https://github.com/uploadcare/blocks/compare/v0.46.2...v0.46.3) (2024-07-01) + +### Bug Fixes + +- **icons:** Added currentColor to icon edit-file ([#689](https://github.com/uploadcare/blocks/issues/689)) ([617d3cd](https://github.com/uploadcare/blocks/commit/617d3cd8e0cde8ecadd6024ce321b7ba507009f1)) + +## [0.46.2](https://github.com/uploadcare/blocks/compare/v0.46.1...v0.46.2) (2024-07-01) + +### Bug Fixes + +- **icons:** added icon edit file ([#687](https://github.com/uploadcare/blocks/issues/687)) ([2a869ab](https://github.com/uploadcare/blocks/commit/2a869abfdbe571aff52ccc5687a8c66e0d829cb5)) + +## [0.46.1](https://github.com/uploadcare/blocks/compare/v0.46.0...v0.46.1) (2024-06-28) + +### Bug Fixes + +- **events:** Added status after deleting files in idle ([#684](https://github.com/uploadcare/blocks/issues/684)) ([0e07ee3](https://github.com/uploadcare/blocks/commit/0e07ee3d82a326e56cab343dafdf7472f2d73216)) + +# [0.46.0](https://github.com/uploadcare/blocks/compare/v0.45.0...v0.46.0) (2024-06-24) ### Features -- added `collectionValidators` and `fileValidators`: Custom validators are now supported for collections and files. This enhancement allows for the addition of necessary checks for uploaded files and collections, providing flexibility and control over compliance with requirements ([#667](https://github.com/uploadcare/blocks/issues/667)) ([d3260b0](https://github.com/uploadcare/blocks/commit/d3260b0cce5ac6ca7cfd0aeb8aff0c9fc35036ed)). See docs [here](https://uploadcare.com/docs/file-uploader/validators). +- Significant improvements to accessibility and keyboard navigation, enhancing user experience and inclusivity ([#671](https://github.com/uploadcare/blocks/issues/671)) ([4acb8a0](https://github.com/uploadcare/blocks/commit/4acb8a0b7ea9c7a95be415627f7d4e1eb748fcf2)) + +# [0.45.0](https://github.com/uploadcare/blocks/compare/v0.44.0...v0.45.0) (2024-06-23) + +### BEAKING CHANGES + +- The previously deprecated API method `setUploadMetadata` has been removed. Use `metadata` instance property on `lr-config` block instead. See [metadata](https://uploadcare.com/docs/file-uploader/options/#metadata) for more details. +- The previously deprecated API method `addFiles` has been removed. Use `addFileFromObject`, `addFileFromUrl` or `addFileFromUuid` instead. See [File Uploader API](https://uploadcare.com/docs/file-uploader/api/#add-file-from-object) for more details. + +See the [migration guide](https://uploadcare.com/docs/file-uploader/migration-to-0.45.0/) for details. + +# [0.44.0](https://github.com/uploadcare/blocks/compare/v0.43.0...v0.44.0) (2024-06-21) + +### BEAKING CHANGES + +- All theme variables of the previous version are deprecated and won't affect the look anymore. +- The default button that opens the uploader dialog (SimpleBtn) component now uses independent variables. +- By default, the theme now uses OKLCH color space. You can still override it using other color spaces, but we recommend converting your colors to OKLCH. + +See the [migration guide](https://uploadcare.com/docs/file-uploader/migration-to-0.44.0/) for details. + +Full styling docs are available [here](https://uploadcare.com/docs/file-uploader/styling/). + +### Features + +- updated theming mechanics with oklch colors and reworked css properties ([#662](https://github.com/uploadcare/blocks/issues/662)) ([ee90e66](https://github.com/uploadcare/blocks/commit/ee90e66c076e21e1ee92b2f4c60f8eb955a7d5bc)) + +# [0.43.0](https://github.com/uploadcare/blocks/compare/v0.42.1...v0.43.0) (2024-06-14) + +### Features +- added `collectionValidators` and `fileValidators`: Custom validators are now supported for collections and files. This enhancement allows for the addition of necessary checks for uploaded files and collections, providing flexibility and control over compliance with requirements ([#667](https://github.com/uploadcare/blocks/issues/667)) ([d3260b0](https://github.com/uploadcare/blocks/commit/d3260b0cce5ac6ca7cfd0aeb8aff0c9fc35036ed)). See docs [here](https://uploadcare.com/docs/file-uploader/validators). ## [0.42.1](https://github.com/uploadcare/blocks/compare/v0.42.0...v0.42.1) (2024-05-30) diff --git a/README.md b/README.md index 81d4d5a66..ec138ac17 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ and [even more](<(https://uploadcare.com/features/?ref=github-readme)>). See Uploadcare Blocks [in action](https://codesandbox.io/p/devbox/github/uploadcare/blocks-examples/tree/main/examples/js-uploader)! -Uploadcare Blocks examples +Uploadcare Blocks examples ## Core features diff --git a/abstract/ActivityBlock.js b/abstract/ActivityBlock.js index 99e0c453d..2dbbbc1bc 100644 --- a/abstract/ActivityBlock.js +++ b/abstract/ActivityBlock.js @@ -8,7 +8,9 @@ const ACTIVE_ATTR = 'active'; const ACTIVE_PROP = '___ACTIVITY_IS_ACTIVE___'; export class ActivityBlock extends Block { + /** @protected */ historyTracked = false; + init$ = activityBlockCtx(this); _debouncedHistoryFlush = debounce(this._historyFlush.bind(this), 10); @@ -37,6 +39,7 @@ export class ActivityBlock extends Block { }); } + /** @protected */ initCallback() { super.initCallback(); if (this.hasAttribute('current-activity')) { @@ -131,6 +134,7 @@ export class ActivityBlock extends Block { ActivityBlock._activityCallbacks.delete(this); } + /** @protected */ destroyCallback() { super.destroyCallback(); this._isActivityRegistered() && this.unregisterActivity(); @@ -138,14 +142,13 @@ export class ActivityBlock extends Block { /** @type {string | null} */ const currentActivity = this.$['*currentActivity']; - /** @type {Set} */ - let blocksRegistry = this.$['*blocksRegistry']; - const hasCurrentActivityInCtx = !![...blocksRegistry].find( + const hasCurrentActivityInCtx = !![...this.blocksRegistry].find( (block) => block instanceof ActivityBlock && block.activityType === currentActivity, ); if (!hasCurrentActivityInCtx) { this.$['*currentActivity'] = null; + this.setOrAddState('*modalActive', false); } } @@ -177,10 +180,8 @@ export class ActivityBlock extends Block { } let couldOpenActivity = !!nextActivity; if (nextActivity) { - /** @type {Set} */ - let blocksRegistry = this.$['*blocksRegistry']; - const nextActivityBlock = [...blocksRegistry].find((block) => block.activityType === nextActivity); - couldOpenActivity = nextActivityBlock?.couldOpenActivity ?? false; + const nextActivityBlock = [...this.blocksRegistry].find((block) => block.activityType === nextActivity); + couldOpenActivity = /** @type {ActivityBlock} */ (nextActivityBlock)?.couldOpenActivity ?? false; } nextActivity = couldOpenActivity ? nextActivity : undefined; this.$['*currentActivity'] = nextActivity; @@ -198,10 +199,9 @@ ActivityBlock.activities = Object.freeze({ DRAW: 'draw', UPLOAD_LIST: 'upload-list', URL: 'url', - CONFIRMATION: 'confirmation', CLOUD_IMG_EDIT: 'cloud-image-edit', EXTERNAL: 'external', DETAILS: 'details', }); -/** @typedef {(typeof ActivityBlock)['activities'][keyof (typeof ActivityBlock)['activities']] | null} ActivityType */ +/** @typedef {(typeof ActivityBlock)['activities'][keyof (typeof ActivityBlock)['activities']] | (string & {}) | null} ActivityType */ diff --git a/abstract/Block.js b/abstract/Block.js index bfa658b23..7bb841a46 100644 --- a/abstract/Block.js +++ b/abstract/Block.js @@ -12,6 +12,7 @@ import { blockCtx } from './CTX.js'; import { LocaleManager, localeStateKey } from './LocaleManager.js'; import { l10nProcessor } from './l10nProcessor.js'; import { sharedConfigKey } from './sharedConfigKey.js'; +import { A11y } from './a11y.js'; const TAG_PREFIX = 'lr-'; @@ -19,9 +20,13 @@ const TAG_PREFIX = 'lr-'; export class Block extends BaseComponent { /** @type {string | null} */ static StateConsumerScope = null; - static className = ''; + + /** @type {string[]} */ + static styleAttrs = []; + + /** @protected */ requireCtxName = false; - allowCustomTemplate = true; + /** @type {import('./ActivityBlock.js').ActivityType} */ activityType = null; @@ -49,6 +54,7 @@ export class Block extends BaseComponent { } /** + * @private * @param {string} key * @param {number} count * @returns {string} @@ -62,6 +68,7 @@ export class Block extends BaseComponent { /** * @param {string} key * @param {() => void} resolver + * @protected */ bindL10n(key, resolver) { this.localeManager?.bindL10n(this, key, resolver); @@ -94,10 +101,7 @@ export class Block extends BaseComponent { * @returns {Boolean} */ hasBlockInCtx(callback) { - // @ts-ignore TODO: fix this - /** @type {Set} */ - let blocksRegistry = this.$['*blocksRegistry']; - for (let block of blocksRegistry) { + for (let block of this.blocksRegistry) { if (callback(block)) { return true; } @@ -118,20 +122,12 @@ export class Block extends BaseComponent { ); } - /** @param {import('./ActivityBlock.js').ActivityType} activityType */ - setActivity(activityType) { - if (this.hasBlockInCtx((b) => b.activityType === activityType)) { - this.$['*currentActivity'] = activityType; - return; - } - console.warn(`Activity type "${activityType}" not found in the context`); - } - + /** @protected */ connectedCallback() { - const className = /** @type {typeof Block} */ (this.constructor).className; - if (className) { - this.classList.toggle(`${TAG_PREFIX}${className}`, true); - } + const styleAttrs = /** @type {typeof Block} */ (this.constructor).styleAttrs; + styleAttrs.forEach((attr) => { + this.setAttribute(attr, ''); + }); if (this.hasAttribute('retpl')) { // @ts-ignore TODO: fix this @@ -158,11 +154,13 @@ export class Block extends BaseComponent { WindowHeightTracker.registerClient(this); } + /** @protected */ disconnectedCallback() { super.disconnectedCallback(); WindowHeightTracker.unregisterClient(this); } + /** @protected */ initCallback() { if (!this.has('*blocksRegistry')) { this.add('*blocksRegistry', new Set()); @@ -178,20 +176,41 @@ export class Block extends BaseComponent { this.add('*localeManager', new LocaleManager(this)); } + if (!this.has('*a11y')) { + this.add('*a11y', new A11y()); + } + this.sub(localeStateKey('locale-id'), (localeId) => { - this.style.direction = getLocaleDirection(localeId); + const direction = getLocaleDirection(localeId); + this.style.direction = direction === 'ltr' ? '' : direction; }); } - /** @returns {LocaleManager | null} */ + /** + * @private + * @returns {LocaleManager | null} + */ get localeManager() { return this.has('*localeManager') ? this.$['*localeManager'] : null; } + /** + * @returns {A11y | null} + * @protected + */ + get a11y() { + return this.has('*a11y') ? this.$['*a11y'] : null; + } + + /** @type {Set} */ + get blocksRegistry() { + return this.$['*blocksRegistry']; + } + + /** @protected */ destroyCallback() { - /** @type {Set} */ - let blocksRegistry = this.$['*blocksRegistry']; - blocksRegistry.delete(this); + let blocksRegistry = this.blocksRegistry; + blocksRegistry?.delete(this); this.localeManager?.destroyL10nBindings(this); this.l10nProcessorSubs = new Map(); @@ -200,7 +219,7 @@ export class Block extends BaseComponent { // TODO: this should be done inside symbiote Data.deleteCtx(this); - if (blocksRegistry.size === 0) { + if (blocksRegistry?.size === 0) { setTimeout(() => { // Destroy global context after all blocks are destroyed and all callbacks are run this.destroyCtxCallback(); @@ -220,25 +239,10 @@ export class Block extends BaseComponent { } /** - * @param {Number} bytes - * @param {Number} [decimals] + * @param {String} url + * @returns {Promise} + * @protected */ - fileSizeFmt(bytes, decimals = 2) { - let units = ['B', 'KB', 'MB', 'GB', 'TB']; - /** - * @param {String} str - * @returns {String} - */ - if (bytes === 0) { - return `0 ${units[0]}`; - } - let k = 1024; - let dm = decimals < 0 ? 0 : decimals; - let i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / k ** i).toFixed(dm)) + ' ' + units[i]; - } - - /** @param {string} url */ async proxyUrl(url) { if (this.cfg.secureDeliveryProxy && this.cfg.secureDeliveryProxyUrlResolver) { console.warn( diff --git a/abstract/CTX.js b/abstract/CTX.js index fd349d240..4462de40b 100644 --- a/abstract/CTX.js +++ b/abstract/CTX.js @@ -6,14 +6,14 @@ export const blockCtx = () => ({}); /** @param {import('./Block').Block} fnCtx */ export const activityBlockCtx = (fnCtx) => ({ ...blockCtx(), - '*currentActivity': '', + '*currentActivity': null, '*currentActivityParams': {}, '*history': [], '*historyBack': null, '*closeModal': () => { fnCtx.set$({ + '*currentActivity': null, '*modalActive': false, - '*currentActivity': '', }); }, }); @@ -24,9 +24,7 @@ export const uploaderBlockCtx = (fnCtx) => ({ '*commonProgress': 0, '*uploadList': [], '*focusedEntry': null, - '*uploadMetadata': null, '*uploadQueue': new Queue(1), - '*uploadCollection': null, /** @type {ReturnType[]} */ '*collectionErrors': [], /** @type {import('../types').OutputCollectionState | null} */ diff --git a/abstract/SolutionBlock.js b/abstract/SolutionBlock.js index d0d5ed423..4f5e7eed7 100644 --- a/abstract/SolutionBlock.js +++ b/abstract/SolutionBlock.js @@ -1,12 +1,18 @@ -import { uploaderBlockCtx } from './CTX.js'; import svgIconsSprite from '../blocks/themes/lr-basic/svg-sprite.js'; import { Block } from './Block.js'; +import { uploaderBlockCtx } from './CTX.js'; export class SolutionBlock extends Block { + static styleAttrs = ['lr-wgt-common']; requireCtxName = true; init$ = uploaderBlockCtx(this); _template = null; + initCallback() { + super.initCallback(); + this.a11y?.registerBlock(this); + } + static set template(value) { this._template = svgIconsSprite + value + /** HTML */ ``; } diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index fadb23975..efc04d298 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -6,54 +6,26 @@ import { uploadFileGroup } from '@uploadcare/upload-client'; import { calculateMaxCenteredCropFrame } from '../blocks/CloudImageEditor/src/crop-utils.js'; import { parseCropPreset } from '../blocks/CloudImageEditor/src/lib/parseCropPreset.js'; import { EventType } from '../blocks/UploadCtxProvider/EventEmitter.js'; -import { UploadSource } from '../blocks/utils/UploadSource.js'; -import { serializeCsv } from '../blocks/utils/comma-separated.js'; import { debounce } from '../blocks/utils/debounce.js'; import { customUserAgent } from '../blocks/utils/userAgent.js'; import { createCdnUrl, createCdnUrlModifiers } from '../utils/cdn-utils.js'; -import { IMAGE_ACCEPT_LIST, fileIsImage, mergeFileTypes } from '../utils/fileTypes.js'; -import { stringToArray } from '../utils/stringToArray.js'; -import { warnOnce } from '../utils/warnOnce.js'; import { uploaderBlockCtx } from './CTX.js'; -import { TypedCollection } from './TypedCollection.js'; -import { buildOutputCollectionState } from './buildOutputCollectionState.js'; -import { uploadEntrySchema } from './uploadEntrySchema.js'; -import { parseCdnUrl } from '../utils/parseCdnUrl.js'; import { SecureUploadsManager } from './SecureUploadsManager.js'; +import { TypedCollection } from './TypedCollection.js'; +import { UploaderPublicApi } from './UploaderPublicApi.js'; import { ValidationManager } from './ValidationManager.js'; +import { uploadEntrySchema } from './uploadEntrySchema.js'; export class UploaderBlock extends ActivityBlock { + /** @protected */ couldBeCtxOwner = false; + + /** @private */ isCtxOwner = false; init$ = uploaderBlockCtx(this); /** @private */ - __initialUploadMetadata = null; - - /** - * This is Public JS API method. Could be called before block initialization, so we need to delay state interactions - * until block init. - * - * TODO: If we add more public methods, it is better to use the single queue instead of tons of private fields per - * each method. See https://github.com/uploadcare/blocks/pull/162/ - * - * @deprecated Use `metadata` instance property on `lr-config` block instead. - * @param {import('@uploadcare/upload-client').Metadata} metadata - * @public - */ - setUploadMetadata(metadata) { - warnOnce( - 'setUploadMetadata is deprecated. Use `metadata` instance property on `lr-config` block instead. See migration guide: https://uploadcare.com/docs/file-uploader/migration-to-0.25.0/', - ); - if (!this.connectedOnce) { - // @ts-ignore TODO: fix this - this.__initialUploadMetadata = metadata; - } else { - this.$['*uploadMetadata'] = metadata; - } - } - get hasCtxOwner() { return this.hasBlockInCtx((block) => { if (block instanceof UploaderBlock) { @@ -63,17 +35,22 @@ export class UploaderBlock extends ActivityBlock { }); } + /** @protected */ initCallback() { super.initCallback(); - if (!this.$['*uploadCollection']) { + if (!this.has('*uploadCollection')) { let uploadCollection = new TypedCollection({ typedSchema: uploadEntrySchema, watchList: ['uploadProgress', 'uploadError', 'fileInfo', 'errors', 'cdnUrl', 'isUploading'], }); - this.$['*uploadCollection'] = uploadCollection; + this.add('*uploadCollection', uploadCollection); } - // + + if (!this.has('*publicApi')) { + this.add('*publicApi', new UploaderPublicApi(this)); + } + if (!this.has('*validationManager')) { this.add('*validationManager', new ValidationManager(this)); } @@ -83,11 +60,38 @@ export class UploaderBlock extends ActivityBlock { } } - /** @returns {ValidationManager | null} */ + /** + * @returns {ValidationManager} + * @protected + */ get validationManager() { - return this.has('*validationManager') ? this.$['*validationManager'] : null; + if (!this.has('*validationManager')) { + throw new Error('Unexpected error: ValidationManager is not initialized'); + } + return this.$['*validationManager']; + } + + /** @returns {UploaderPublicApi} */ + get api() { + if (!this.has('*publicApi')) { + throw new Error('Unexpected error: UploaderPublicApi is not initialized'); + } + return this.$['*publicApi']; + } + + getAPI() { + return this.api; } + /** @returns {TypedCollection} */ + get uploadCollection() { + if (!this.has('*uploadCollection')) { + throw new Error('Unexpected error: TypedCollection is not initialized'); + } + return this.$['*uploadCollection']; + } + + /** @protected */ destroyCtxCallback() { this._unobserveCollectionProperties?.(); this._unobserveCollection?.(); @@ -97,6 +101,7 @@ export class UploaderBlock extends ActivityBlock { super.destroyCtxCallback(); } + /** @private */ initCtxOwner() { this.isCtxOwner = true; @@ -112,240 +117,19 @@ export class UploaderBlock extends ActivityBlock { this.$['*uploadQueue'].concurrency = Number(value) || 1; }); - if (this.__initialUploadMetadata) { - this.$['*uploadMetadata'] = this.__initialUploadMetadata; - } - if (!this.$['*secureUploadsManager']) { this.$['*secureUploadsManager'] = new SecureUploadsManager(this); } - } - - // TODO: Probably we should not allow user to override `source` property - - /** - * @param {string} url - * @param {{ silent?: boolean; fileName?: string; source?: string }} [options] - * @returns {import('../types').OutputFileEntry<'idle'>} - */ - addFileFromUrl(url, { silent, fileName, source } = {}) { - const internalId = this.uploadCollection.add({ - externalUrl: url, - fileName: fileName ?? null, - silent: silent ?? false, - source: source ?? UploadSource.API, - }); - return this.getOutputItem(internalId); - } - - /** - * @param {string} uuid - * @param {{ silent?: boolean; fileName?: string; source?: string }} [options] - * @returns {import('../types').OutputFileEntry<'idle'>} - */ - addFileFromUuid(uuid, { silent, fileName, source } = {}) { - const internalId = this.uploadCollection.add({ - uuid, - fileName: fileName ?? null, - silent: silent ?? false, - source: source ?? UploadSource.API, - }); - return this.getOutputItem(internalId); - } - - /** - * @param {string} cdnUrl - * @param {{ silent?: boolean; fileName?: string; source?: string }} [options] - * @returns {import('../types').OutputFileEntry<'idle'>} - */ - addFileFromCdnUrl(cdnUrl, { silent, fileName, source } = {}) { - const parsedCdnUrl = parseCdnUrl({ url: cdnUrl, cdnBase: this.cfg.cdnCname }); - if (!parsedCdnUrl) { - throw new Error('Invalid CDN URL'); - } - const internalId = this.uploadCollection.add({ - uuid: parsedCdnUrl.uuid, - cdnUrl, - cdnUrlModifiers: parsedCdnUrl.cdnUrlModifiers, - fileName: fileName ?? parsedCdnUrl.filename ?? null, - silent: silent ?? false, - source: source ?? UploadSource.API, - }); - return this.getOutputItem(internalId); - } - - /** - * @param {File} file - * @param {{ silent?: boolean; fileName?: string; source?: string; fullPath?: string }} [options] - * @returns {import('../types').OutputFileEntry<'idle'>} - */ - addFileFromObject(file, { silent, fileName, source, fullPath } = {}) { - const internalId = this.uploadCollection.add({ - file, - isImage: fileIsImage(file), - mimeType: file.type, - fileName: fileName ?? file.name, - fileSize: file.size, - silent: silent ?? false, - source: source ?? UploadSource.API, - fullPath: fullPath ?? null, - }); - return this.getOutputItem(internalId); - } - - /** - * @deprecated Will be removed in the near future. Please use `addFileFromObject`, `addFileFromUrl` or - * `addFileFromUuid` instead. - * @param {File[]} files - * @returns {import('../types').OutputFileEntry<'idle'>[]} - */ - addFiles(files) { - console.warn( - '`addFiles` method is deprecated. Please use `addFileFromObject`, `addFileFromUrl` or `addFileFromUuid` instead.', - ); - return files.map((/** @type {File} */ file) => { - const internalId = this.uploadCollection.add({ - file, - isImage: fileIsImage(file), - mimeType: file.type, - fileName: file.name, - fileSize: file.size, - }); - return this.getOutputItem(internalId); - }); - } - - /** @param {string} internalId */ - removeFileByInternalId(internalId) { - if (!this.uploadCollection.read(internalId)) { - throw new Error(`File with internalId ${internalId} not found`); - } - this.uploadCollection.remove(internalId); - } - - removeAllFiles() { - this.uploadCollection.clearAll(); - } - - uploadAll = () => { - const itemsToUpload = this.uploadCollection.items().filter((id) => { - const entry = this.uploadCollection.read(id); - return !entry.getValue('isRemoved') && !entry.getValue('isUploading') && !entry.getValue('fileInfo'); - }); - - if (itemsToUpload.length === 0) { - return; - } - - this.$['*uploadTrigger'] = new Set(itemsToUpload); - this.emit( - EventType.COMMON_UPLOAD_START, - /** @type {import('../types').OutputCollectionState<'uploading'>} */ (this.getOutputCollectionState()), - ); - }; - - /** @param {{ captureCamera?: boolean }} options */ - openSystemDialog(options = {}) { - let accept = serializeCsv(mergeFileTypes([this.cfg.accept ?? '', ...(this.cfg.imgOnly ? IMAGE_ACCEPT_LIST : [])])); - - if (this.cfg.accept && !!this.cfg.imgOnly) { - console.warn( - 'There could be a mistake.\n' + - 'Both `accept` and `imgOnly` parameters are set.\n' + - 'The value of `accept` will be concatenated with the internal image mime types list.', - ); - } - this.fileInput = document.createElement('input'); - this.fileInput.type = 'file'; - this.fileInput.multiple = this.cfg.multiple; - if (options.captureCamera) { - this.fileInput.capture = this.cfg.cameraCapture; - this.fileInput.accept = 'image/*'; - } else { - this.fileInput.accept = accept; - } - this.fileInput.dispatchEvent(new MouseEvent('click')); - this.fileInput.onchange = () => { - // @ts-ignore TODO: fix this - [...this.fileInput['files']].forEach((file) => - this.addFileFromObject(file, { source: options.captureCamera ? UploadSource.CAMERA : UploadSource.LOCAL }), - ); - // To call uploadTrigger UploadList should draw file items first: - this.$['*currentActivity'] = ActivityBlock.activities.UPLOAD_LIST; - this.setOrAddState('*modalActive', true); - // @ts-ignore TODO: fix this - this.fileInput['value'] = ''; - this.fileInput = null; - }; - } - /** @type {string[]} */ - get sourceList() { - /** @type {string[]} */ - let list = []; - if (this.cfg.sourceList) { - list = stringToArray(this.cfg.sourceList); - } - // @ts-ignore TODO: fix this - return list; - } - - /** @param {Boolean} [force] */ - initFlow(force = false) { - if (this.uploadCollection.size > 0 && !force) { - this.set$({ - '*currentActivity': ActivityBlock.activities.UPLOAD_LIST, - }); - this.setOrAddState('*modalActive', true); - } else { - if (this.sourceList?.length === 1) { - const srcKey = this.sourceList[0]; - - // TODO: We should refactor those handlers - if (srcKey === 'local') { - this.$['*currentActivity'] = ActivityBlock.activities.UPLOAD_LIST; - this?.['openSystemDialog'](); - return; + if (this.has('*modalActive')) { + this.sub('*modalActive', (modalActive) => { + if (modalActive && !this.$['*currentActivity']) { + this.$['*modalActive'] = false; } - - /** @type {Set} */ - const blocksRegistry = this.$['*blocksRegistry']; - /** - * @param {import('./Block').Block} block - * @returns {block is import('../blocks/SourceBtn/SourceBtn.js').SourceBtn} - */ - const isSourceBtn = (block) => 'type' in block && block.type === srcKey; - const sourceBtnBlock = [...blocksRegistry].find(isSourceBtn); - // TODO: This is weird that we have this logic inside UI component, we should consider to move it somewhere else - sourceBtnBlock?.activate(); - if (this.$['*currentActivity']) { - this.setOrAddState('*modalActive', true); - } - } else { - // Multiple sources case: - this.set$({ - '*currentActivity': ActivityBlock.activities.START_FROM, - }); - this.setOrAddState('*modalActive', true); - } - } - } - - doneFlow() { - this.set$({ - '*currentActivity': this.doneActivity, - '*history': this.doneActivity ? [this.doneActivity] : [], - }); - if (!this.$['*currentActivity']) { - this.setOrAddState('*modalActive', false); + }); } } - /** @returns {TypedCollection} */ - get uploadCollection() { - return this.$['*uploadCollection']; - } - /** * @private * @param {import('../types').OutputCollectionState} collectionState @@ -363,10 +147,10 @@ export class UploaderBlock extends ActivityBlock { } this.$['*groupInfo'] = resp; const collectionStateWithGroup = /** @type {import('../types').OutputCollectionState<'success', 'has-group'>} */ ( - this.getOutputCollectionState() + this.api.getOutputCollectionState() ); this.emit(EventType.GROUP_CREATED, collectionStateWithGroup); - this.emit(EventType.CHANGE, () => this.getOutputCollectionState(), { debounce: true }); + this.emit(EventType.CHANGE, () => this.api.getOutputCollectionState(), { debounce: true }); this.$['*collectionState'] = collectionStateWithGroup; } @@ -376,9 +160,9 @@ export class UploaderBlock extends ActivityBlock { if (data.length !== this.uploadCollection.size) { return; } - const collectionState = this.getOutputCollectionState(); + const collectionState = this.api.getOutputCollectionState(); this.$['*collectionState'] = collectionState; - this.emit(EventType.CHANGE, () => this.getOutputCollectionState(), { debounce: true }); + this.emit(EventType.CHANGE, () => this.api.getOutputCollectionState(), { debounce: true }); if (this.cfg.groupOutput && collectionState.totalCount > 0 && collectionState.status === 'success') { this._createGroup(collectionState); @@ -394,14 +178,12 @@ export class UploaderBlock extends ActivityBlock { if (added.size || removed.size) { this.$['*groupInfo'] = null; } - if (this.validationManager) { - this.validationManager.runFileValidators(); - this.validationManager.runCollectionValidators(); - } + this.validationManager.runFileValidators(); + this.validationManager.runCollectionValidators(); for (const entry of added) { if (!entry.getValue('silent')) { - this.emit(EventType.FILE_ADDED, this.getOutputItem(entry.uid)); + this.emit(EventType.FILE_ADDED, this.api.getOutputItem(entry.uid)); } } @@ -416,7 +198,7 @@ export class UploaderBlock extends ActivityBlock { uploadProgress: 0, }); URL.revokeObjectURL(entry?.getValue('thumbUrl')); - this.emit(EventType.FILE_REMOVED, this.getOutputItem(entry.uid)); + this.emit(EventType.FILE_REMOVED, this.api.getOutputItem(entry.uid)); } this.$['*uploadList'] = entries.map((uid) => { @@ -447,14 +229,14 @@ export class UploaderBlock extends ActivityBlock { entriesToRunValidation.length > 0 && setTimeout(() => { // We can't modify entry properties in the same tick, so we need to wait a bit - if (this.validationManager) this.validationManager.runFileValidators(entriesToRunValidation); + this.validationManager.runFileValidators(entriesToRunValidation); }); if (changeMap.uploadProgress) { for (const entryId of changeMap.uploadProgress) { const { isUploading, silent } = Data.getCtx(entryId).store; if (isUploading && !silent) { - this.emit(EventType.FILE_UPLOAD_PROGRESS, this.getOutputItem(entryId)); + this.emit(EventType.FILE_UPLOAD_PROGRESS, this.api.getOutputItem(entryId)); } } @@ -464,7 +246,7 @@ export class UploaderBlock extends ActivityBlock { for (const entryId of changeMap.isUploading) { const { isUploading, silent } = Data.getCtx(entryId).store; if (isUploading && !silent) { - this.emit(EventType.FILE_UPLOAD_START, this.getOutputItem(entryId)); + this.emit(EventType.FILE_UPLOAD_START, this.api.getOutputItem(entryId)); } } } @@ -472,44 +254,50 @@ export class UploaderBlock extends ActivityBlock { for (const entryId of changeMap.fileInfo) { const { fileInfo, silent } = Data.getCtx(entryId).store; if (fileInfo && !silent) { - this.emit(EventType.FILE_UPLOAD_SUCCESS, this.getOutputItem(entryId)); + this.emit(EventType.FILE_UPLOAD_SUCCESS, this.api.getOutputItem(entryId)); } } if (this.cfg.cropPreset) { this.setInitialCrop(); } - let loadedItems = uploadCollection.findItems((entry) => { - return !!entry.getValue('fileInfo'); - }); - let errorItems = uploadCollection.findItems((entry) => { - return entry.getValue('errors').length > 0; - }); - if (errorItems.length === 0 && uploadCollection.size === loadedItems.length) { - this.emit( - EventType.COMMON_UPLOAD_SUCCESS, - /** @type {import('../types').OutputCollectionState<'success'>} */ (this.getOutputCollectionState()), - ); - } } if (changeMap.errors) { for (const entryId of changeMap.errors) { const { errors } = Data.getCtx(entryId).store; if (errors.length > 0) { - this.emit(EventType.FILE_UPLOAD_FAILED, this.getOutputItem(entryId)); + this.emit(EventType.FILE_UPLOAD_FAILED, this.api.getOutputItem(entryId)); this.emit( EventType.COMMON_UPLOAD_FAILED, - () => /** @type {import('../types').OutputCollectionState<'failed'>} */ (this.getOutputCollectionState()), + () => + /** @type {import('../types').OutputCollectionState<'failed'>} */ (this.api.getOutputCollectionState()), { debounce: true }, ); } } + const loadedItems = uploadCollection.findItems((entry) => { + return !!entry.getValue('fileInfo'); + }); + const errorItems = uploadCollection.findItems((entry) => { + return entry.getValue('errors').length > 0; + }); + if ( + uploadCollection.size > 0 && + errorItems.length === 0 && + uploadCollection.size === loadedItems.length && + this.$['*collectionErrors'].length === 0 + ) { + this.emit( + EventType.COMMON_UPLOAD_SUCCESS, + /** @type {import('../types').OutputCollectionState<'success'>} */ (this.api.getOutputCollectionState()), + ); + } } if (changeMap.cdnUrl) { const uids = [...changeMap.cdnUrl].filter((uid) => { return !!this.uploadCollection.read(uid)?.getValue('cdnUrl'); }); uids.forEach((uid) => { - this.emit(EventType.FILE_URL_CHANGED, this.getOutputItem(uid)); + this.emit(EventType.FILE_URL_CHANGED, this.api.getOutputItem(uid)); }); this.$['*groupInfo'] = null; @@ -535,7 +323,7 @@ export class UploaderBlock extends ActivityBlock { this.$['*commonProgress'] = progress; this.emit( EventType.COMMON_UPLOAD_PROGRESS, - /** @type {import('../types').OutputCollectionState<'uploading'>} */ (this.getOutputCollectionState()), + /** @type {import('../types').OutputCollectionState<'uploading'>} */ (this.api.getOutputCollectionState()), ); }; @@ -584,16 +372,19 @@ export class UploaderBlock extends ActivityBlock { * @protected */ async getMetadataFor(entryId) { - const configValue = this.cfg.metadata ?? /** @type {import('../types').Metadata} */ (this.$['*uploadMetadata']); + const configValue = this.cfg.metadata || undefined; if (typeof configValue === 'function') { - const outputFileEntry = this.getOutputItem(entryId); + const outputFileEntry = this.api.getOutputItem(entryId); const metadata = await configValue(outputFileEntry); return metadata; } return configValue; } - /** @returns {Promise} */ + /** + * @returns {Promise} + * @protected + */ async getUploadClientOptions() { /** @type {SecureUploadsManager} */ const secureUploadsManager = this.$['*secureUploadsManager']; @@ -620,69 +411,12 @@ export class UploaderBlock extends ActivityBlock { return options; } - /** - * @template {import('../types').OutputFileStatus} TStatus - * @param {string} entryId - * @returns {import('../types/exported.js').OutputFileEntry} - */ - getOutputItem(entryId) { - const uploadEntryData = /** @type {import('./uploadEntrySchema.js').UploadEntry} */ (Data.getCtx(entryId).store); - - /** @type {import('@uploadcare/upload-client').UploadcareFile?} */ - const fileInfo = uploadEntryData.fileInfo; - - /** @type {import('../types').OutputFileEntry['status']} */ - let status = uploadEntryData.isRemoved - ? 'removed' - : uploadEntryData.errors.length > 0 - ? 'failed' - : !!uploadEntryData.fileInfo - ? 'success' - : uploadEntryData.isUploading - ? 'uploading' - : 'idle'; - - /** @type {unknown} */ - const outputItem = { - uuid: fileInfo?.uuid ?? uploadEntryData.uuid ?? null, - internalId: entryId, - name: fileInfo?.originalFilename ?? uploadEntryData.fileName, - size: fileInfo?.size ?? uploadEntryData.fileSize, - isImage: fileInfo?.isImage ?? uploadEntryData.isImage, - mimeType: fileInfo?.mimeType ?? uploadEntryData.mimeType, - file: uploadEntryData.file, - externalUrl: uploadEntryData.externalUrl, - cdnUrlModifiers: uploadEntryData.cdnUrlModifiers, - cdnUrl: uploadEntryData.cdnUrl ?? fileInfo?.cdnUrl ?? null, - fullPath: uploadEntryData.fullPath, - uploadProgress: uploadEntryData.uploadProgress, - fileInfo: fileInfo ?? null, - metadata: uploadEntryData.metadata ?? fileInfo?.metadata ?? null, - isSuccess: status === 'success', - isUploading: status === 'uploading', - isFailed: status === 'failed', - isRemoved: status === 'removed', - errors: /** @type {import('../types/exported.js').OutputFileEntry['errors']} */ (uploadEntryData.errors), - status, - }; - - return /** @type {import('../types/exported.js').OutputFileEntry} */ (outputItem); - } - - /** - * @param {(item: import('./TypedData.js').TypedData) => Boolean} [checkFn] - * @returns {import('../types/exported.js').OutputFileEntry[]} - */ - getOutputData(checkFn) { - const entriesIds = checkFn ? this.uploadCollection.findItems(checkFn) : this.uploadCollection.items(); - const data = entriesIds.map((itemId) => this.getOutputItem(itemId)); + /** @returns {import('../types/exported.js').OutputFileEntry[]} */ + getOutputData() { + const entriesIds = this.uploadCollection.items(); + const data = entriesIds.map((itemId) => this.api.getOutputItem(itemId)); return data; } - - /** @template {import('../types').OutputCollectionStatus} TStatus */ - getOutputCollectionState() { - return /** @type {ReturnType>} */ (buildOutputCollectionState(this)); - } } /** @enum {String} */ diff --git a/abstract/UploaderPublicApi.js b/abstract/UploaderPublicApi.js new file mode 100644 index 000000000..daef7e8a9 --- /dev/null +++ b/abstract/UploaderPublicApi.js @@ -0,0 +1,316 @@ +// @ts-check +import { ActivityBlock } from './ActivityBlock.js'; + +import { Data } from '@symbiotejs/symbiote'; +import { EventType } from '../blocks/UploadCtxProvider/EventEmitter.js'; +import { UploadSource } from '../blocks/utils/UploadSource.js'; +import { serializeCsv } from '../blocks/utils/comma-separated.js'; +import { IMAGE_ACCEPT_LIST, fileIsImage, mergeFileTypes } from '../utils/fileTypes.js'; +import { parseCdnUrl } from '../utils/parseCdnUrl.js'; +import { buildOutputCollectionState } from './buildOutputCollectionState.js'; +import { stringToArray } from '../utils/stringToArray.js'; + +export class UploaderPublicApi { + /** + * @private + * @type {import('./UploaderBlock.js').UploaderBlock} + */ + _ctx; + + /** @param {import('./UploaderBlock.js').UploaderBlock} ctx */ + constructor(ctx) { + this._ctx = ctx; + } + + /** @private */ + get _uploadCollection() { + return this._ctx.uploadCollection; + } + + get cfg() { + return this._ctx.cfg; + } + + get l10n() { + return this._ctx.l10n.bind(this._ctx); + } + + /** + * TODO: Probably we should not allow user to override `source` property + * + * @param {string} url + * @param {{ silent?: boolean; fileName?: string; source?: string }} [options] + * @returns {import('../types').OutputFileEntry<'idle'>} + */ + addFileFromUrl = (url, { silent, fileName, source } = {}) => { + const internalId = this._uploadCollection.add({ + externalUrl: url, + fileName: fileName ?? null, + silent: silent ?? false, + source: source ?? UploadSource.API, + }); + return this.getOutputItem(internalId); + }; + + /** + * @param {string} uuid + * @param {{ silent?: boolean; fileName?: string; source?: string }} [options] + * @returns {import('../types').OutputFileEntry<'idle'>} + */ + addFileFromUuid = (uuid, { silent, fileName, source } = {}) => { + const internalId = this._uploadCollection.add({ + uuid, + fileName: fileName ?? null, + silent: silent ?? false, + source: source ?? UploadSource.API, + }); + return this.getOutputItem(internalId); + }; + + /** + * @param {string} cdnUrl + * @param {{ silent?: boolean; fileName?: string; source?: string }} [options] + * @returns {import('../types').OutputFileEntry<'idle'>} + */ + addFileFromCdnUrl = (cdnUrl, { silent, fileName, source } = {}) => { + const parsedCdnUrl = parseCdnUrl({ url: cdnUrl, cdnBase: this.cfg.cdnCname }); + if (!parsedCdnUrl) { + throw new Error('Invalid CDN URL'); + } + const internalId = this._uploadCollection.add({ + uuid: parsedCdnUrl.uuid, + cdnUrl, + cdnUrlModifiers: parsedCdnUrl.cdnUrlModifiers, + fileName: fileName ?? parsedCdnUrl.filename ?? null, + silent: silent ?? false, + source: source ?? UploadSource.API, + }); + return this.getOutputItem(internalId); + }; + + /** + * @param {File} file + * @param {{ silent?: boolean; fileName?: string; source?: string; fullPath?: string }} [options] + * @returns {import('../types').OutputFileEntry<'idle'>} + */ + addFileFromObject = (file, { silent, fileName, source, fullPath } = {}) => { + const internalId = this._uploadCollection.add({ + file, + isImage: fileIsImage(file), + mimeType: file.type, + fileName: fileName ?? file.name, + fileSize: file.size, + silent: silent ?? false, + source: source ?? UploadSource.API, + fullPath: fullPath ?? null, + }); + return this.getOutputItem(internalId); + }; + + /** @param {string} internalId */ + removeFileByInternalId = (internalId) => { + if (!this._uploadCollection.read(internalId)) { + throw new Error(`File with internalId ${internalId} not found`); + } + this._uploadCollection.remove(internalId); + }; + + removeAllFiles() { + this._uploadCollection.clearAll(); + } + + uploadAll = () => { + const itemsToUpload = this._uploadCollection.items().filter((id) => { + const entry = this._uploadCollection.read(id); + return !entry.getValue('isRemoved') && !entry.getValue('isUploading') && !entry.getValue('fileInfo'); + }); + + if (itemsToUpload.length === 0) { + return; + } + + this._ctx.$['*uploadTrigger'] = new Set(itemsToUpload); + this._ctx.emit( + EventType.COMMON_UPLOAD_START, + /** @type {import('../types').OutputCollectionState<'uploading'>} */ (this.getOutputCollectionState()), + ); + }; + + /** @param {{ captureCamera?: boolean }} options */ + openSystemDialog = (options = {}) => { + let accept = serializeCsv(mergeFileTypes([this.cfg.accept ?? '', ...(this.cfg.imgOnly ? IMAGE_ACCEPT_LIST : [])])); + + if (this.cfg.accept && !!this.cfg.imgOnly) { + console.warn( + 'There could be a mistake.\n' + + 'Both `accept` and `imgOnly` parameters are set.\n' + + 'The value of `accept` will be concatenated with the internal image mime types list.', + ); + } + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.multiple = this.cfg.multiple; + if (options.captureCamera) { + fileInput.capture = this.cfg.cameraCapture; + fileInput.accept = 'image/*'; + } else { + fileInput.accept = accept; + } + fileInput.dispatchEvent(new MouseEvent('click')); + fileInput.onchange = () => { + // @ts-ignore TODO: fix this + [...fileInput['files']].forEach((file) => + this.addFileFromObject(file, { source: options.captureCamera ? UploadSource.CAMERA : UploadSource.LOCAL }), + ); + // To call uploadTrigger UploadList should draw file items first: + this._ctx.$['*currentActivity'] = ActivityBlock.activities.UPLOAD_LIST; + this._ctx.setOrAddState('*modalActive', true); + // @ts-ignore TODO: fix this + fileInput['value'] = ''; + }; + }; + + /** + * @template {import('../types').OutputFileStatus} TStatus + * @param {string} entryId + * @returns {import('../types/exported.js').OutputFileEntry} + */ + getOutputItem = (entryId) => { + const uploadEntryData = /** @type {import('./uploadEntrySchema.js').UploadEntry} */ (Data.getCtx(entryId).store); + + /** @type {import('@uploadcare/upload-client').UploadcareFile?} */ + const fileInfo = uploadEntryData.fileInfo; + + /** @type {import('../types').OutputFileEntry['status']} */ + let status = uploadEntryData.isRemoved + ? 'removed' + : uploadEntryData.errors.length > 0 + ? 'failed' + : !!uploadEntryData.fileInfo + ? 'success' + : uploadEntryData.isUploading + ? 'uploading' + : 'idle'; + + /** @type {unknown} */ + const outputItem = { + uuid: fileInfo?.uuid ?? uploadEntryData.uuid ?? null, + internalId: entryId, + name: fileInfo?.originalFilename ?? uploadEntryData.fileName, + size: fileInfo?.size ?? uploadEntryData.fileSize, + isImage: fileInfo?.isImage ?? uploadEntryData.isImage, + mimeType: fileInfo?.mimeType ?? uploadEntryData.mimeType, + file: uploadEntryData.file, + externalUrl: uploadEntryData.externalUrl, + cdnUrlModifiers: uploadEntryData.cdnUrlModifiers, + cdnUrl: uploadEntryData.cdnUrl ?? fileInfo?.cdnUrl ?? null, + fullPath: uploadEntryData.fullPath, + uploadProgress: uploadEntryData.uploadProgress, + fileInfo: fileInfo ?? null, + metadata: uploadEntryData.metadata ?? fileInfo?.metadata ?? null, + isSuccess: status === 'success', + isUploading: status === 'uploading', + isFailed: status === 'failed', + isRemoved: status === 'removed', + errors: /** @type {import('../types/exported.js').OutputFileEntry['errors']} */ (uploadEntryData.errors), + status, + }; + + return /** @type {import('../types/exported.js').OutputFileEntry} */ (outputItem); + }; + + /** @template {import('../types').OutputCollectionStatus} TStatus */ + getOutputCollectionState = () => { + return /** @type {ReturnType>} */ ( + buildOutputCollectionState(this._ctx) + ); + }; + + /** @param {Boolean} [force] */ + initFlow = (force = false) => { + if (this._uploadCollection.size > 0 && !force) { + this._ctx.set$({ + '*currentActivity': ActivityBlock.activities.UPLOAD_LIST, + }); + this._ctx.setOrAddState('*modalActive', true); + } else { + if (this._sourceList?.length === 1) { + const srcKey = this._sourceList[0]; + + // TODO: We should refactor those handlers + if (srcKey === 'local') { + this._ctx.$['*currentActivity'] = ActivityBlock.activities.UPLOAD_LIST; + this.openSystemDialog(); + return; + } + + /** @type {Set} */ + const blocksRegistry = this._ctx.$['*blocksRegistry']; + /** + * @param {import('./Block').Block} block + * @returns {block is import('../blocks/SourceBtn/SourceBtn.js').SourceBtn} + */ + const isSourceBtn = (block) => 'type' in block && block.type === srcKey; + const sourceBtnBlock = [...blocksRegistry].find(isSourceBtn); + // TODO: This is weird that we have this logic inside UI component, we should consider to move it somewhere else + sourceBtnBlock?.activate(); + if (this._ctx.$['*currentActivity']) { + this._ctx.setOrAddState('*modalActive', true); + } + } else { + // Multiple sources case: + this._ctx.set$({ + '*currentActivity': ActivityBlock.activities.START_FROM, + }); + this._ctx.setOrAddState('*modalActive', true); + } + } + }; + + doneFlow = () => { + this._ctx.set$({ + '*currentActivity': this._ctx.doneActivity, + '*history': this._ctx.doneActivity ? [this._ctx.doneActivity] : [], + }); + if (!this._ctx.$['*currentActivity']) { + this._ctx.setOrAddState('*modalActive', false); + } + }; + + /** + * @param {import('./ActivityBlock.js').ActivityType} activityType + * @param {import('../blocks/ExternalSource/ExternalSource.js').ActivityParams | {}} [params] + */ + setCurrentActivity = (activityType, params = {}) => { + if (this._ctx.hasBlockInCtx((b) => b.activityType === activityType)) { + this._ctx.set$({ + '*currentActivityParams': params, + '*currentActivity': activityType, + }); + return; + } + console.warn(`Activity type "${activityType}" not found in the context`); + }; + + /** @param {boolean} opened */ + setModalState = (opened) => { + if (opened && !this._ctx.$['*currentActivity']) { + console.warn(`Can't open modal without current activity. Please use "setCurrentActivity" method first.`); + return; + } + this._ctx.setOrAddState('*modalActive', opened); + }; + + /** + * @private + * @type {string[]} + */ + get _sourceList() { + /** @type {string[]} */ + let list = []; + if (this.cfg.sourceList) { + list = stringToArray(this.cfg.sourceList); + } + return list; + } +} diff --git a/abstract/ValidationManager.js b/abstract/ValidationManager.js index 045573bfa..0574a15dd 100644 --- a/abstract/ValidationManager.js +++ b/abstract/ValidationManager.js @@ -11,7 +11,7 @@ import { validateMultiple, validateCollectionUploadError } from '../utils/valida /** * @typedef {( * outputEntry: import('../types').OutputFileEntry, - * ctx: import('./UploaderBlock.js').UploaderBlock, + * api: import('./UploaderPublicApi.js').UploaderPublicApi, * ) => undefined | import('../types').OutputErrorFile} FuncFileValidator */ @@ -22,7 +22,7 @@ import { validateMultiple, validateCollectionUploadError } from '../utils/valida * import('../types').OutputCollectionStatus * > * >, - * ctx: import('./UploaderBlock.js').UploaderBlock, + * api: import('./UploaderPublicApi.js').UploaderPublicApi, * ) => undefined | import('../types').OutputErrorCollection} FuncCollectionValidator */ @@ -76,7 +76,7 @@ export class ValidationManager { } runCollectionValidators() { - const collection = this._blockInstance.getOutputCollectionState(); + const collection = this._blockInstance.api.getOutputCollectionState(); const errors = []; for (const validator of [ @@ -84,7 +84,7 @@ export class ValidationManager { ...this._addCustomTypeToValidators(this._blockInstance.cfg.collectionValidators), ]) { try { - const errorOrErrors = validator(collection, this._blockInstance); + const errorOrErrors = validator(collection, this._blockInstance.api); if (!errorOrErrors) { continue; } @@ -107,7 +107,7 @@ export class ValidationManager { EventType.COMMON_UPLOAD_FAILED, () => /** @type {import('../types').OutputCollectionState<'failed'>} */ ( - this._blockInstance.getOutputCollectionState() + this._blockInstance.api.getOutputCollectionState() ), { debounce: true }, ); @@ -119,7 +119,7 @@ export class ValidationManager { * @param {import('./TypedData.js').TypedData} entry */ _runFileValidatorsForEntry(entry) { - const outputEntry = this._blockInstance.getOutputItem(entry.uid); + const outputEntry = this._blockInstance.api.getOutputItem(entry.uid); const errors = []; for (const validator of [ @@ -127,7 +127,7 @@ export class ValidationManager { ...this._addCustomTypeToValidators(this._blockInstance.cfg.fileValidators), ]) { try { - const error = validator(outputEntry, this._blockInstance); + const error = validator(outputEntry, this._blockInstance.api); if (!error) { continue; } diff --git a/abstract/a11y.js b/abstract/a11y.js new file mode 100644 index 000000000..f29f1dce6 --- /dev/null +++ b/abstract/a11y.js @@ -0,0 +1,118 @@ +// @ts-check +import { startKeyUX, hiddenKeyUX, jumpKeyUX, focusGroupKeyUX, pressKeyUX } from 'keyux'; + +/** + * MinimalWindow interface is not exported by keyux, so we import it here using tricky way. + * + * @typedef {Parameters[0]} MinimalWindow + */ + +/** + * This is global window wrapper that allows to scope event listeners to a specific part of the DOM. + * + * It is used to scope the key UX to the widget. + * + * @implements {MinimalWindow} + */ +class ScopedMinimalWindow { + /** + * @private + * @type {Map void>} + */ + _listeners = new Map(); + + /** + * @private + * @type {Node[]} + */ + _scope = []; + + /** + * @param {'keydown' | 'keyup'} type + * @param {(event: Event) => void} listener + */ + addEventListener(type, listener) { + /** @param {Event} e */ + const wrappedListener = (e) => { + const target = e.target; + if (!target) return; + if (this._scope.some((el) => el === e.target || el.contains(/** @type {Node} */ (target)))) { + listener(e); + } + }; + this._listeners.set(listener, wrappedListener); + window.addEventListener(type, wrappedListener); + } + + /** + * @param {'keydown' | 'keyup'} type + * @param {(event: {}) => void} listener + */ + removeEventListener(type, listener) { + const wrappedListener = this._listeners.get(listener); + if (wrappedListener) { + window.removeEventListener(type, wrappedListener); + } + this._listeners.delete(listener); + } + + get CustomEvent() { + return window.CustomEvent; + } + + get document() { + return window.document; + } + + get navigator() { + return window.navigator; + } + + /** @param {Node} scope */ + registerScope(scope) { + this._scope.push(scope); + } + + destroy() { + this._scope = []; + this._listeners.forEach((listener, originalListener) => { + window.removeEventListener('keydown', listener); + window.removeEventListener('keyup', listener); + this._listeners.delete(originalListener); + }); + } +} + +export class A11y { + /** + * @private + * @type {(() => void) | undefined} + */ + _destroyKeyUX; + + /** + * @private + * @type {ScopedMinimalWindow} + */ + _scopedWindow; + + constructor() { + this._scopedWindow = new ScopedMinimalWindow(); + this._destroyKeyUX = startKeyUX(this._scopedWindow, [ + focusGroupKeyUX(), + pressKeyUX('is-pressed'), + jumpKeyUX(), + hiddenKeyUX(), + ]); + } + + /** @param {import('./Block.js').Block} scope */ + registerBlock(scope) { + this._scopedWindow.registerScope(scope); + } + + destroy() { + this._destroyKeyUX?.(); + this._scopedWindow.destroy(); + } +} diff --git a/abstract/buildOutputCollectionState.js b/abstract/buildOutputCollectionState.js index f49b5ccba..9ae9f4787 100644 --- a/abstract/buildOutputCollectionState.js +++ b/abstract/buildOutputCollectionState.js @@ -75,7 +75,11 @@ export function buildOutputCollectionState(uploaderBlock) { }, isSuccess: () => { - return state.errors.length === 0 && state.successEntries.length === state.allEntries.length; + return ( + state.allEntries.length > 0 && + state.errors.length === 0 && + state.successEntries.length === state.allEntries.length + ); }, isUploading: () => { diff --git a/blocks/ActivityHeader/activity-header.css b/blocks/ActivityHeader/activity-header.css index 50a236602..94f19bf92 100644 --- a/blocks/ActivityHeader/activity-header.css +++ b/blocks/ActivityHeader/activity-header.css @@ -1,34 +1,14 @@ lr-activity-header { display: flex; - gap: var(--gap-mid); justify-content: space-between; - padding: var(--gap-mid); - color: var(--clr-txt); + gap: var(--uc-padding); + padding: var(--uc-padding); + color: var(--uc-foreground); font-weight: 500; font-size: 1em; - line-height: var(--ui-size); -} - -lr-activity-header lr-icon { - height: var(--ui-size); } lr-activity-header > * { display: flex; align-items: center; } - -lr-activity-header button { - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--clr-txt-mid); -} - -lr-activity-header button:hover { - background-color: var(--clr-background); -} - -lr-activity-header button:active { - background-color: var(--clr-background-dark); -} diff --git a/blocks/CameraSource/CameraSource.js b/blocks/CameraSource/CameraSource.js index af01923aa..5ce68f2e3 100644 --- a/blocks/CameraSource/CameraSource.js +++ b/blocks/CameraSource/CameraSource.js @@ -66,7 +66,7 @@ export class CameraSource extends UploaderBlock { * @param {'granted' | 'denied' | 'prompt'} state */ _setPermissionsState = debounce((state) => { - this.classList.toggle('initialized', state === 'granted'); + this.classList.toggle('uc-initialized', state === 'granted'); if (state === 'granted') { this.set$({ @@ -171,7 +171,7 @@ export class CameraSource extends UploaderBlock { lastModified: date, type: format, }); - this.addFileFromObject(file, { source: UploadSource.CAMERA }); + this.api.addFileFromObject(file, { source: UploadSource.CAMERA }); this.set$({ '*currentActivity': ActivityBlock.activities.UPLOAD_LIST, }); @@ -215,7 +215,7 @@ export class CameraSource extends UploaderBlock { CameraSource.template = /* HTML */ ` -
@@ -223,22 +223,22 @@ CameraSource.template = /* HTML */ `
-
-
+
-
+
-
diff --git a/blocks/CameraSource/camera-source.css b/blocks/CameraSource/camera-source.css index 15353cd75..a8b3ddaaf 100644 --- a/blocks/CameraSource/camera-source.css +++ b/blocks/CameraSource/camera-source.css @@ -6,23 +6,23 @@ lr-camera-source { height: 100%; max-height: 100%; overflow: hidden; - background-color: var(--clr-background-light); - border-radius: var(--border-radius-element); + background-color: var(--uc-background); + border-radius: var(--uc-radius); } -lr-modal lr-camera-source { - width: min(calc(var(--modal-max-w) - var(--gap-mid) * 2), calc(100vw - var(--gap-mid) * 2)); +[lr-modal] lr-camera-source { + width: min(calc(var(--uc-dialog-max-width) - var(--uc-padding) * 2), calc(100vw - var(--uc-padding) * 2)); height: 100vh; max-height: var(--modal-max-content-height); } -lr-camera-source.initialized { +lr-camera-source.uc-initialized { height: max-content; } @media only screen and (max-width: 430px) { lr-camera-source { - width: calc(100vw - var(--gap-mid) * 2); + width: calc(100vw - var(--uc-padding) * 2); height: var(--modal-content-height-fill, 100%); } } @@ -33,74 +33,75 @@ lr-camera-source video { max-height: 100%; object-fit: contain; object-position: center center; - background-color: var(--clr-background-dark); - border-radius: var(--border-radius-element); + background-color: var(--uc-muted); + border-radius: var(--uc-radius); } -lr-camera-source .toolbar { +lr-camera-source .uc-toolbar { position: absolute; bottom: 0; display: flex; justify-content: space-between; width: 100%; - padding: var(--gap-mid); - background-color: var(--clr-background-light); + padding: var(--uc-padding); + background-color: var(--uc-background); } -lr-camera-source .content { +lr-camera-source .uc-content { display: flex; flex: 1; justify-content: center; width: 100%; - padding: var(--gap-mid); + padding: var(--uc-padding); padding-top: 0; overflow: hidden; } -lr-camera-source .message-box { - --padding: calc(var(--gap-max) * 2); - +lr-camera-source .uc-message-box { display: flex; flex-direction: column; - grid-gap: var(--gap-max); + grid-gap: 40px; align-items: center; justify-content: center; - padding: var(--padding) var(--padding) 0 var(--padding); - color: var(--clr-txt); + padding: 40px 40px 0; + color: var(--uc-foreground); } -lr-camera-source .message-box button { - color: var(--clr-btn-txt-primary); - background-color: var(--clr-btn-bgr-primary); +lr-camera-source .uc-message-box button { + color: var(--uc-primary-foreground); + background-color: var(--uc-primary); } -lr-camera-source .shot-btn { +lr-camera-source .uc-shot-btn { position: absolute; - bottom: var(--gap-max); - width: calc(var(--ui-size) * 1.8); - height: calc(var(--ui-size) * 1.8); - color: var(--clr-background-light); - background-color: var(--clr-txt); + bottom: 20px; + width: 58px; + height: 58px; + color: var(--uc-background); + background-color: var(--uc-foreground); border-radius: 50%; opacity: 0.85; - transition: var(--transition-duration) ease; + transition: + bottom var(--uc-transition), + opacity var(--uc-transition), + transform var(--uc-transition); } -lr-camera-source .shot-btn:hover { +lr-camera-source .uc-shot-btn:hover { transform: scale(1.05); opacity: 1; } -lr-camera-source .shot-btn:active { - background-color: var(--clr-txt-mid); +lr-camera-source .uc-shot-btn:active { + transform: scale(1); opacity: 1; } -lr-camera-source .shot-btn[disabled] { - bottom: calc(var(--gap-max) * -1 - var(--gap-mid) - var(--ui-size) * 2); +lr-camera-source .uc-shot-btn[disabled] { + bottom: -80px; } -lr-camera-source .shot-btn lr-icon svg { - width: calc(var(--ui-size) / 1.5); - height: calc(var(--ui-size) / 1.5); +lr-camera-source .uc-shot-btn lr-icon svg { + width: 20px; + height: 20px; } diff --git a/blocks/CloudImageEditor/src/CloudImageEditorBlock.js b/blocks/CloudImageEditor/src/CloudImageEditorBlock.js index 036b74e51..2f824abe5 100644 --- a/blocks/CloudImageEditor/src/CloudImageEditorBlock.js +++ b/blocks/CloudImageEditor/src/CloudImageEditorBlock.js @@ -19,7 +19,7 @@ import { TabId } from './toolbar-constants.js'; export class CloudImageEditorBlock extends Block { ctxOwner = true; - static className = 'cloud-image-editor'; + static styleAttrs = ['lr-cloud-image-editor']; constructor() { super(); @@ -173,13 +173,13 @@ export class CloudImageEditorBlock extends Block { ); this.sub('*tabId', (tabId) => { - this.ref['img-el'].className = classNames('image', { - image_hidden_to_cropper: tabId === TabId.CROP, - image_hidden_effects: tabId !== TabId.CROP, + this.ref['img-el'].className = classNames('uc-image', { + 'uc-image_hidden_to_cropper': tabId === TabId.CROP, + 'uc-image_hidden_effects': tabId !== TabId.CROP, }); }); - this.classList.add('editor_ON'); + this.classList.add('uc-editor_ON'); this.sub('*networkProblems', (networkProblems) => { this.$['presence.networkProblems'] = networkProblems; diff --git a/blocks/CloudImageEditor/src/CropFrame.js b/blocks/CloudImageEditor/src/CropFrame.js index 26e483216..334afd647 100644 --- a/blocks/CloudImageEditor/src/CropFrame.js +++ b/blocks/CloudImageEditor/src/CropFrame.js @@ -187,9 +187,9 @@ export class CropFrame extends Block { let disableThumb = this._shouldThumbBeDisabled(direction); groupNode.setAttribute( 'class', - classNames('thumb', { - 'thumb--hidden': disableThumb, - 'thumb--visible': !disableThumb, + classNames('uc-thumb', { + 'uc-thumb--hidden': disableThumb, + 'uc-thumb--visible': !disableThumb, }), ); } @@ -220,7 +220,7 @@ export class CropFrame extends Block { for (let j = 0; j < 3; j++) { let direction = /** @type {import('./types.js').Direction} */ (`${['n', '', 's'][i]}${['w', '', 'e'][j]}`); let groupNode = createSvgNode('g'); - groupNode.classList.add('thumb'); + groupNode.classList.add('uc-thumb'); groupNode.setAttribute('with-effects', ''); let interactionNode = createSvgNode('rect', { fill: 'transparent', @@ -256,7 +256,7 @@ export class CropFrame extends Block { width: '100%', height: '100%', fill: 'none', - stroke: '#000000', + stroke: 'currentColor', 'stroke-width': GUIDE_STROKE_WIDTH, 'stroke-opacity': 0.5, }); @@ -268,7 +268,7 @@ export class CropFrame extends Block { y1: `0%`, x2: `${GUIDE_THIRD * i}%`, y2: `100%`, - stroke: '#000000', + stroke: 'currentColor', 'stroke-width': GUIDE_STROKE_WIDTH, 'stroke-opacity': 0.3, }); @@ -281,14 +281,14 @@ export class CropFrame extends Block { y1: `${GUIDE_THIRD * i}%`, x2: `100%`, y2: `${GUIDE_THIRD * i}%`, - stroke: '#000000', + stroke: 'currentColor', 'stroke-width': GUIDE_STROKE_WIDTH, 'stroke-opacity': 0.3, }); svg.appendChild(line); } - svg.classList.add('guides', 'guides--semi-hidden'); + svg.classList.add('uc-guides', 'uc-guides--semi-hidden'); return svg; } @@ -452,9 +452,9 @@ export class CropFrame extends Block { .forEach((groupNode) => { groupNode.setAttribute( 'class', - classNames('thumb', { - 'thumb--hidden': !visible, - 'thumb--visible': visible, + classNames('uc-thumb', { + 'uc-thumb--hidden': !visible, + 'uc-thumb--visible': visible, }), ); }); @@ -488,9 +488,9 @@ export class CropFrame extends Block { this._frameGuides.setAttribute( 'class', classNames({ - 'guides--hidden': this._guidesHidden, - 'guides--visible': !this._guidesHidden && dragging, - 'guides--semi-hidden': !this._guidesHidden && !dragging, + 'uc-guides--hidden': this._guidesHidden, + 'uc-guides--visible': !this._guidesHidden && dragging, + 'uc-guides--semi-hidden': !this._guidesHidden && !dragging, }), ); }); @@ -508,4 +508,4 @@ export class CropFrame extends Block { } } -CropFrame.template = /* HTML */ ` `; +CropFrame.template = /* HTML */ ` `; diff --git a/blocks/CloudImageEditor/src/EditorButtonControl.js b/blocks/CloudImageEditor/src/EditorButtonControl.js index e8cf27665..82ecff6f5 100644 --- a/blocks/CloudImageEditor/src/EditorButtonControl.js +++ b/blocks/CloudImageEditor/src/EditorButtonControl.js @@ -16,11 +16,6 @@ export class EditorButtonControl extends Block { this._titleEl = this.ref['title-el']; this._iconEl = this.ref['icon-el']; - this.setAttribute('role', 'button'); - if (this.tabIndex === -1) { - this.tabIndex = 0; - } - this.sub('title', (title) => { let titleEl = this._titleEl; if (titleEl) { @@ -30,8 +25,8 @@ export class EditorButtonControl extends Block { this.sub('active', (active) => { this.className = classNames({ - active: active, - not_active: !active, + 'uc-active': active, + 'uc-not_active': !active, }); }); @@ -42,7 +37,8 @@ export class EditorButtonControl extends Block { } EditorButtonControl.template = /* HTML */ ` -
- -
{{title}}
+ `; diff --git a/blocks/CloudImageEditor/src/EditorFilterControl.js b/blocks/CloudImageEditor/src/EditorFilterControl.js index 2c0ed8572..f706191e1 100644 --- a/blocks/CloudImageEditor/src/EditorFilterControl.js +++ b/blocks/CloudImageEditor/src/EditorFilterControl.js @@ -110,7 +110,7 @@ export class EditorFilterControl extends EditorButtonControl { this._originalUrl = originalUrl; if (this.$.isOriginal) { - this.ref['icon-el'].classList.add('original-icon'); + this.ref['icon-el'].classList.add('uc-original-icon'); } else { this._observer.observe(this); } @@ -158,7 +158,8 @@ export class EditorFilterControl extends EditorButtonControl { } EditorFilterControl.template = /* HTML */ ` -
-
- + `; diff --git a/blocks/CloudImageEditor/src/EditorImageCropper.js b/blocks/CloudImageEditor/src/EditorImageCropper.js index 4c9a59706..039d6b5ab 100644 --- a/blocks/CloudImageEditor/src/EditorImageCropper.js +++ b/blocks/CloudImageEditor/src/EditorImageCropper.js @@ -258,9 +258,9 @@ export class EditorImageCropper extends Block { this._transitionToImage(); setTimeout(() => { this.className = classNames({ - active_from_viewer: fromViewer, - active_from_editor: !fromViewer, - inactive_to_editor: false, + 'uc-active_from_viewer': fromViewer, + 'uc-active_from_editor': !fromViewer, + 'uc-inactive_to_editor': false, }); }); } @@ -413,9 +413,9 @@ export class EditorImageCropper extends Block { this._transitionToCrop(); this.className = classNames({ - active_from_viewer: false, - active_from_editor: false, - inactive_to_editor: true, + 'uc-active_from_viewer': false, + 'uc-active_from_editor': false, + 'uc-inactive_to_editor': true, }); this.ref['frame-el'].toggleThumbs(false); @@ -546,6 +546,6 @@ export class EditorImageCropper extends Block { } EditorImageCropper.template = /* HTML */ ` - + `; diff --git a/blocks/CloudImageEditor/src/EditorImageFader.js b/blocks/CloudImageEditor/src/EditorImageFader.js index d46f956c9..e93c4f0a6 100644 --- a/blocks/CloudImageEditor/src/EditorImageFader.js +++ b/blocks/CloudImageEditor/src/EditorImageFader.js @@ -88,7 +88,7 @@ export class EditorImageFader extends Block { /** @private */ this._addKeypointDebounced = debounce(this._addKeypoint.bind(this), 600); - this.classList.add('inactive_to_cropper'); + this.classList.add('uc-inactive_to_cropper'); } /** @@ -201,7 +201,7 @@ export class EditorImageFader extends Block { image.addEventListener('load', stop, { once: true }); image.addEventListener('error', stop, { once: true }); keypoint.image = image; - image.classList.add('fader-image'); + image.classList.add('uc-fader-image'); image.addEventListener( 'load', @@ -264,7 +264,7 @@ export class EditorImageFader extends Block { /** @private */ _createPreviewImage() { let image = new Image(); - image.classList.add('fader-image', 'fader-image--preview'); + image.classList.add('uc-fader-image', 'uc-fader-image--preview'); image.style.opacity = '0'; return image; } @@ -298,7 +298,7 @@ export class EditorImageFader extends Block { this._container = container; this._keypoints.forEach((kp, idx) => { let kpImage = images[idx]; - kpImage.classList.add('fader-image'); + kpImage.classList.add('uc-fader-image'); kp.image = kpImage; this._container.appendChild(kpImage); }); @@ -354,10 +354,11 @@ export class EditorImageFader extends Block { if (image.src === src) { image.style.opacity = '1'; image.style.transform = 'scale(1)'; + this.className = classNames({ - active_from_viewer: this._fromViewer, - active_from_cropper: !this._fromViewer, - inactive_to_cropper: false, + 'uc-active_from_viewer': this._fromViewer, + 'uc-active_from_cropper': !this._fromViewer, + 'uc-inactive_to_cropper': false, }); return; } @@ -372,10 +373,11 @@ export class EditorImageFader extends Block { if (image) { image.style.opacity = '1'; image.style.transform = 'scale(1)'; + this.className = classNames({ - active_from_viewer: this._fromViewer, - active_from_cropper: !this._fromViewer, - inactive_to_cropper: false, + 'uc-active_from_viewer': this._fromViewer, + 'uc-active_from_cropper': !this._fromViewer, + 'uc-inactive_to_cropper': false, }); } }, @@ -434,10 +436,11 @@ export class EditorImageFader extends Block { if (this._previewImage) { this._previewImage.style.transform = 'scale(1)'; } + this.className = classNames({ - active_from_viewer: false, - active_from_cropper: false, - inactive_to_cropper: true, + 'uc-active_from_viewer': false, + 'uc-active_from_cropper': false, + 'uc-inactive_to_cropper': true, }); this.addEventListener( 'transitionend', diff --git a/blocks/CloudImageEditor/src/EditorScroller.js b/blocks/CloudImageEditor/src/EditorScroller.js index 8338f417e..ccdb9f6c2 100644 --- a/blocks/CloudImageEditor/src/EditorScroller.js +++ b/blocks/CloudImageEditor/src/EditorScroller.js @@ -6,14 +6,26 @@ export class EditorScroller extends Block { initCallback() { super.initCallback(); - this.addEventListener('wheel', (e) => { - e.preventDefault(); - let { deltaY, deltaX } = e; - if (Math.abs(deltaX) > X_THRESHOLD) { - this.scrollLeft += deltaX; - } else { - this.scrollLeft += deltaY; - } + this.addEventListener( + 'wheel', + (e) => { + e.preventDefault(); + + let { deltaY, deltaX } = e; + if (Math.abs(deltaX) > X_THRESHOLD) { + this.scrollLeft += deltaX; + } else { + this.scrollLeft += deltaY; + } + }, + { + passive: false, + }, + ); + + // This fixes some strange bug on MacOS - wheel event doesn't fire for physical mouse wheel if no scroll event attached also + this.addEventListener('scroll', () => {}, { + passive: true, }); } } diff --git a/blocks/CloudImageEditor/src/EditorToolbar.js b/blocks/CloudImageEditor/src/EditorToolbar.js index 9a70d8631..19c7d76f0 100644 --- a/blocks/CloudImageEditor/src/EditorToolbar.js +++ b/blocks/CloudImageEditor/src/EditorToolbar.js @@ -19,14 +19,16 @@ import { viewerImageSrc } from './util.js'; /** @param {String} id */ function renderTabToggle(id) { return /* HTML */ ` - + @@ -36,10 +38,19 @@ function renderTabToggle(id) { /** @param {String} id */ function renderTabContent(id) { return /* HTML */ ` - + -
-
+
+
@@ -74,24 +85,24 @@ export class EditorToolbar extends Block { 'presence.tabToggle.tuning': true, 'presence.tabToggle.filters': true, 'presence.subTopToolbarStyles': { - hidden: 'sub-toolbar--top-hidden', - visible: 'sub-toolbar--visible', + hidden: 'uc-sub-toolbar--top-hidden', + visible: 'uc-sub-toolbar--visible', }, 'presence.subBottomToolbarStyles': { - hidden: 'sub-toolbar--bottom-hidden', - visible: 'sub-toolbar--visible', + hidden: 'uc-sub-toolbar--bottom-hidden', + visible: 'uc-sub-toolbar--visible', }, 'presence.tabContentStyles': { - hidden: 'tab-content--hidden', - visible: 'tab-content--visible', + hidden: 'uc-tab-content--hidden', + visible: 'uc-tab-content--visible', }, 'presence.tabToggleStyles': { - hidden: 'tab-toggle--hidden', - visible: 'tab-toggle--visible', + hidden: 'uc-tab-toggle--hidden', + visible: 'uc-tab-toggle--visible', }, 'presence.tabTogglesStyles': { - hidden: 'tab-toggles--hidden', - visible: 'tab-toggles--visible', + hidden: 'uc-tab-toggles--hidden', + visible: 'uc-tab-toggles--visible', }, 'on.cancel': () => { this._cancelPreload?.(); @@ -115,6 +126,10 @@ export class EditorToolbar extends Block { this._activateTab(id, { fromViewer: false }); } }, + tab_role: 'tab', + [`tab_${TabId.TUNING}`]: `tab_${TabId.TUNING}`, + [`tab_${TabId.CROP}`]: `tab_${TabId.CROP}`, + [`tab_${TabId.FILTERS}`]: `tab_${TabId.FILTERS}`, }; /** @private */ @@ -126,7 +141,7 @@ export class EditorToolbar extends Block { _onSliderClose() { this.$['*showSlider'] = false; if (this.$['*tabId'] === TabId.TUNING) { - this.ref['tooltip-el'].classList.toggle('info-tooltip_visible', false); + this.ref['tooltip-el'].classList.toggle('uc-info-tooltip_visible', false); } } @@ -197,7 +212,7 @@ export class EditorToolbar extends Block { [...fr.children].forEach((el, idx) => { if (idx === fr.childNodes.length - 1) { - el.classList.add('controls-list_last-item'); + el.classList.add('uc-controls-list_last-item'); } }); @@ -300,7 +315,7 @@ export class EditorToolbar extends Block { if (visible) { this.$['*operationTooltip'] = text; } - this.ref['tooltip-el'].classList.toggle('info-tooltip_visible', visible); + this.ref['tooltip-el'].classList.toggle('uc-info-tooltip_visible', visible); }, 0); initCallback() { @@ -386,29 +401,39 @@ export class EditorToolbar extends Block { EditorToolbar.template = /* HTML */ ` -
-
-
{{*operationTooltip}}
+
+
+
{{*operationTooltip}}
-
- -
${ALL_TABS.map(renderTabContent).join('')}
-
- - -
+
+ +
${ALL_TABS.map(renderTabContent).join('')}
+
+ +
${ALL_TABS.map(renderTabToggle).join('')}
- + +
- -
+ +
-
- +
+
diff --git a/blocks/CloudImageEditor/src/css/common.css b/blocks/CloudImageEditor/src/css/common.css index 09b4060d9..c224c83c9 100644 --- a/blocks/CloudImageEditor/src/css/common.css +++ b/blocks/CloudImageEditor/src/css/common.css @@ -1,67 +1,39 @@ /* TODO: we shuoud use basic theme there */ -:host(.lr-cloud-image-editor), -.lr-cloud-image-editor { - /* Theme settings >>> */ - --rgb-primary-accent: 6, 2, 196; - --rgb-text-base: 0, 0, 0; - --rgb-text-accent-contrast: 255, 255, 255; - --rgb-fill-contrast: 255, 255, 255; - --rgb-fill-shaded: 245, 245, 245; - --rgb-shadow: 0, 0, 0; - --rgb-error: 209, 81, 81; - --opacity-shade-mid: 0.2; - - /* <<< Theme settings */ - - --color-primary-accent: rgb(var(--rgb-primary-accent)); - --color-text-base: rgb(var(--rgb-text-base)); - --color-text-accent-contrast: rgb(var(--rgb-text-accent-contrast)); - --color-text-soft: rgb(var(--rgb-fill-contrast)); - --color-text-error: rgb(var(--rgb-error)); - --color-fill-contrast: rgb(var(--rgb-fill-contrast)); - --color-modal-backdrop: rgba(var(--rgb-fill-shaded), 0.95); - --color-image-background: rgba(var(--rgb-fill-shaded)); - --color-outline: rgba(var(--rgb-text-base), var(--opacity-shade-mid)); - --color-underline: rgba(var(--rgb-text-base), 0.08); - --color-shade: rgba(var(--rgb-text-base), 0.02); +[lr-cloud-image-editor] { + --color-primary-accent: var(--uc-primary); + --color-text-base: var(--uc-foreground); + --color-text-accent-contrast: var(--uc-background); + --color-fill-contrast: var(--uc-background); + --color-modal-backdrop: oklch(0 0 0 / 0.1); + --color-image-background: var(--uc-muted); --color-focus-ring: var(--color-primary-accent); - --color-input-placeholder: rgba(var(--rgb-text-base), 0.32); - --color-error: rgb(var(--rgb-error)); + --color-crop-guides: var(--uc-foreground); - --font-size-ui: 16px; - --font-size-title: 18px; - --font-weight-title: 500; - --font-size-soft: 14px; + --font-size-ui: var(--uc-font-size); - --size-touch-area: 40px; - --size-panel-heading: 66px; + --size-touch-area: var(--uc-button-size); + --size-panel-heading: calc(var(--uc-button-size) + var(--uc-padding) * 2); --size-ui-min-width: 130px; --size-line-width: 1px; --size-modal-width: 650px; + --size-icon: calc(var(--uc-button-size) / 2); /* TODO: remove icon size overrides */ - --border-radius-connect: 2px; - --border-radius-editor: 3px; - --border-radius-thumb: 4px; - --border-radius-ui: 5px; - --border-radius-base: 6px; + --border-radius-editor: var(--uc-radius); + --border-radius-thumb: var(--uc-radius); + --border-radius-ui: var(--uc-radius); + --border-radius-base: var(--uc-radius); --cldtr-gap-min: 5px; --cldtr-gap-mid-1: 10px; --cldtr-gap-mid-2: 15px; --cldtr-gap-max: 20px; - --opacity-min: var(--opacity-shade-mid); - --opacity-mid: 0.1; - --opacity-max: 0.05; - --transition-duration-2: var(--transition-duration-all, 0.2s); --transition-duration-3: var(--transition-duration-all, 0.3s); --transition-duration-4: var(--transition-duration-all, 0.4s); --transition-duration-5: var(--transition-duration-all, 0.5s); - --shadow-base: 0px 5px 15px rgba(var(--rgb-shadow), 0.1), 0px 1px 4px rgba(var(--rgb-shadow), 0.15); - --modal-header-opacity: 1; --modal-header-height: var(--size-panel-heading); --modal-toolbar-height: var(--size-panel-heading); @@ -74,61 +46,54 @@ max-height: 100%; } -:host(.lr-cloud-image-editor) :is([can-handle-paste]:hover, [can-handle-paste]:focus), -.lr-cloud-image-editor :is([can-handle-paste]:hover, [can-handle-paste]:focus) { +[lr-cloud-image-editor] :is([can-handle-paste]:hover, [can-handle-paste]:focus) { --can-handle-paste: 'true'; } -:host(.lr-cloud-image-editor) - :is([tabindex][focus-visible], [tabindex]:hover, [with-effects][focus-visible], [with-effects]:hover), -.lr-cloud-image-editor +[lr-cloud-image-editor] :is([tabindex][focus-visible], [tabindex]:hover, [with-effects][focus-visible], [with-effects]:hover) { --filter-effect: var(--hover-filter) !important; --opacity-effect: var(--hover-opacity) !important; --color-effect: var(--hover-color-rgb) !important; + --background-effect: var(--hover-background) !important; } -:host(.lr-cloud-image-editor) :is([tabindex]:active, [with-effects]:active), -.lr-cloud-image-editor :is([tabindex]:active, [with-effects]:active) { +[lr-cloud-image-editor] :is([tabindex]:active, [with-effects]:active) { --filter-effect: var(--down-filter) !important; --opacity-effect: var(--down-opacity) !important; --color-effect: var(--down-color-rgb) !important; + --background-effect: var(--down-background) !important; } -:host(.lr-cloud-image-editor) :is([tabindex][active], [with-effects][active]), -.lr-cloud-image-editor :is([tabindex][active], [with-effects][active]) { +[lr-cloud-image-editor] :is([tabindex][active], [with-effects][active]) { --filter-effect: var(--active-filter) !important; --opacity-effect: var(--active-opacity) !important; --color-effect: var(--active-color-rgb) !important; + --background-effect: var(--active-background) !important; } -:host(.lr-cloud-image-editor) [hidden-scrollbar]::-webkit-scrollbar, -.lr-cloud-image-editor [hidden-scrollbar]::-webkit-scrollbar { +[lr-cloud-image-editor] [hidden-scrollbar]::-webkit-scrollbar { display: none; } -:host(.lr-cloud-image-editor) [hidden-scrollbar], -.lr-cloud-image-editor [hidden-scrollbar] { +[lr-cloud-image-editor] [hidden-scrollbar] { -ms-overflow-style: none; scrollbar-width: none; } -:host(.lr-cloud-image-editor.editor_ON), -.lr-cloud-image-editor.editor_ON { +[lr-cloud-image-editor].uc-editor_ON { --modal-header-opacity: 0; --modal-header-height: 0px; --modal-toolbar-height: calc(var(--size-panel-heading) * 2); } -:host(.lr-cloud-image-editor.editor_OFF), -.lr-cloud-image-editor.editor_OFF { +[lr-cloud-image-editor].uc-editor_OFF { --modal-header-opacity: 1; --modal-header-height: var(--size-panel-heading); --modal-toolbar-height: var(--size-panel-heading); } -:host(.lr-cloud-image-editor) > .wrapper, -.lr-cloud-image-editor > .wrapper { +[lr-cloud-image-editor] > .uc-wrapper { --l-min-img-height: var(--modal-toolbar-height); --l-max-img-height: 100%; --l-edit-button-width: 120px; @@ -143,23 +108,20 @@ } @media only screen and (max-width: 800px) { - :host(.lr-cloud-image-editor) > .wrapper, - .lr-cloud-image-editor > .wrapper { + [lr-cloud-image-editor] > .uc-wrapper { --l-edit-button-width: 70px; --l-toolbar-horizontal-padding: var(--cldtr-gap-min); } } -:host(.lr-cloud-image-editor) > .wrapper > .viewport, -.lr-cloud-image-editor > .wrapper > .viewport { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport { display: flex; align-items: center; justify-content: center; overflow: hidden; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .image_container > .image, -.lr-cloud-image-editor > .wrapper > .viewport > .image_container > .image { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-image_container > .uc-image { --viewer-image-opacity: 1; position: absolute; @@ -178,15 +140,13 @@ pointer-events: auto; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .image_container > .image.image_visible_viewer, -.lr-cloud-image-editor > .wrapper > .viewport > .image_container > .image.image_visible_viewer { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-image_container > .uc-image.uc-image_visible_viewer { transition: opacity var(--transition-duration-3) ease-in-out, transform var(--transition-duration-4); } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .image_container > .image.image_hidden_to_cropper, -.lr-cloud-image-editor > .wrapper > .viewport > .image_container > .image.image_hidden_to_cropper { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-image_container > .uc-image.uc-image_hidden_to_cropper { --viewer-image-opacity: 0; background-image: var(--transparent-pixel); @@ -197,8 +157,7 @@ pointer-events: none; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .image_container > .image.image_hidden_effects, -.lr-cloud-image-editor > .wrapper > .viewport > .image_container > .image.image_hidden_effects { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-image_container > .uc-image.uc-image_hidden_effects { --viewer-image-opacity: 0; transform: scale(1); @@ -208,8 +167,7 @@ pointer-events: none; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .image_container, -.lr-cloud-image-editor > .wrapper > .viewport > .image_container { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-image_container { position: relative; display: block; width: 100%; @@ -218,14 +176,12 @@ transition: var(--transition-duration-3); } -:host(.lr-cloud-image-editor) > .wrapper > .toolbar, -.lr-cloud-image-editor > .wrapper > .toolbar { +[lr-cloud-image-editor] > .uc-wrapper > .uc-toolbar { position: relative; transition: 0.3s; } -:host(.lr-cloud-image-editor) > .wrapper > .toolbar > .toolbar_content, -.lr-cloud-image-editor > .wrapper > .toolbar > .toolbar_content { +[lr-cloud-image-editor] > .uc-wrapper > .uc-toolbar > .uc-toolbar_content { position: absolute; bottom: 0px; left: 0px; @@ -236,8 +192,7 @@ background-color: var(--color-fill-contrast); } -:host(.lr-cloud-image-editor) > .wrapper > .toolbar > .toolbar_content.toolbar_content__viewer, -.lr-cloud-image-editor > .wrapper > .toolbar > .toolbar_content.toolbar_content__viewer { +[lr-cloud-image-editor] > .uc-wrapper > .uc-toolbar > .uc-toolbar_content.uc-toolbar_content__viewer { display: flex; align-items: center; justify-content: space-between; @@ -246,19 +201,16 @@ padding-left: var(--l-toolbar-horizontal-padding); } -:host(.lr-cloud-image-editor) > .wrapper > .toolbar > .toolbar_content.toolbar_content__editor, -.lr-cloud-image-editor > .wrapper > .toolbar > .toolbar_content.toolbar_content__editor { +[lr-cloud-image-editor] > .uc-wrapper > .uc-toolbar > .uc-toolbar_content.uc-toolbar_content__editor { display: flex; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .info_pan, -.lr-cloud-image-editor > .wrapper > .viewport > .info_pan { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-info_pan { position: absolute; user-select: none; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .file_type_outer, -.lr-cloud-image-editor > .wrapper > .viewport > .file_type_outer { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-file_type_outer { position: absolute; z-index: 2; display: flex; @@ -267,13 +219,11 @@ user-select: none; } -:host(.lr-cloud-image-editor) > .wrapper > .viewport > .file_type_outer > .file_type, -.lr-cloud-image-editor > .wrapper > .viewport > .file_type_outer > .file_type { +[lr-cloud-image-editor] > .uc-wrapper > .uc-viewport > .uc-file_type_outer > .uc-file_type { padding: 4px 0.8em; } -:host(.lr-cloud-image-editor) > .wrapper > .network_problems_splash, -.lr-cloud-image-editor > .wrapper > .network_problems_splash { +[lr-cloud-image-editor] > .uc-wrapper > .uc-network_problems_splash { position: absolute; z-index: 4; display: flex; @@ -283,8 +233,7 @@ background-color: var(--color-fill-contrast); } -:host(.lr-cloud-image-editor) > .wrapper > .network_problems_splash > .network_problems_content, -.lr-cloud-image-editor > .wrapper > .network_problems_splash > .network_problems_content { +[lr-cloud-image-editor] > .uc-wrapper > .uc-network_problems_splash > .uc-network_problems_content { display: flex; flex: 1; flex-direction: column; @@ -292,41 +241,39 @@ justify-content: center; } -:host(.lr-cloud-image-editor) - > .wrapper - > .network_problems_splash - > .network_problems_content - > .network_problems_icon, -.lr-cloud-image-editor > .wrapper > .network_problems_splash > .network_problems_content > .network_problems_icon { +[lr-cloud-image-editor] + > .uc-wrapper + > .uc-network_problems_splash + > .uc-network_problems_content + > .uc-network_problems_icon { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; - color: rgba(var(--rgb-text-base), 0.6); - background-color: rgba(var(--rgb-fill-shaded)); + color: var(--uc-foreground); + background-color: var(--uc-muted); border-radius: 50%; } -:host(.lr-cloud-image-editor) - > .wrapper - > .network_problems_splash - > .network_problems_content - > .network_problems_text, -.lr-cloud-image-editor > .wrapper > .network_problems_splash > .network_problems_content > .network_problems_text { +[lr-cloud-image-editor] + > .uc-wrapper + > .uc-network_problems_splash + > .uc-network_problems_content + > .uc-network_problems_text { margin-top: var(--cldtr-gap-max); font-size: var(--font-size-ui); + color: var(--uc-foreground); } -:host(.lr-cloud-image-editor) > .wrapper > .network_problems_splash > .network_problems_footer, -.lr-cloud-image-editor > .wrapper > .network_problems_splash > .network_problems_footer { +[lr-cloud-image-editor] > .uc-wrapper > .uc-network_problems_splash > .uc-network_problems_footer { display: flex; align-items: center; justify-content: center; height: var(--size-panel-heading); } -lr-crop-frame > .svg { +lr-crop-frame > .uc-svg { position: absolute; top: 0px; left: 0px; @@ -339,7 +286,7 @@ lr-crop-frame > .svg { transition: var(--transition-duration-3); } -lr-crop-frame > .thumb { +lr-crop-frame > .uc-thumb { --idle-color-rgb: var(--color-text-base); --hover-color-rgb: var(--color-primary-accent); --focus-color-rgb: var(--color-primary-accent); @@ -352,29 +299,29 @@ lr-crop-frame > .thumb { opacity var(--transition-duration-3); } -lr-crop-frame > .thumb--visible { +lr-crop-frame > .uc-thumb--visible { opacity: 1; pointer-events: auto; } -lr-crop-frame > .thumb--hidden { +lr-crop-frame > .uc-thumb--hidden { opacity: 0; pointer-events: none; } -lr-crop-frame > .guides { +lr-crop-frame > .uc-guides { transition: var(--transition-duration-3); } -lr-crop-frame > .guides--hidden { +lr-crop-frame > .uc-guides--hidden { opacity: 0; } -lr-crop-frame > .guides--semi-hidden { +lr-crop-frame > .uc-guides--semi-hidden { opacity: 0.2; } -lr-crop-frame > .guides--visible { +lr-crop-frame > .uc-guides--visible { opacity: 1; } @@ -382,28 +329,40 @@ lr-editor-button-control, lr-editor-crop-button-control, lr-editor-filter-control, lr-editor-operation-control { - --l-base-min-width: 40px; - --l-base-height: var(--l-base-min-width); + --l-base-min-width: var(--uc-button-size); + --l-base-height: var(--uc-button-size); --opacity-effect: var(--idle-opacity); --color-effect: var(--idle-color-rgb); + --background-effect: var(--idle-background); --filter-effect: var(--idle-filter); - --idle-color-rgb: var(--rgb-text-base); - --idle-opacity: 0.05; + + --idle-color-rgb: var(--uc-secondary-foreground); + --idle-opacity: 1; --idle-filter: 1; - --hover-color-rgb: var(--idle-color-rgb); - --hover-opacity: 0.08; + --idle-background: var(--uc-secondary); + --hover-color-rgb: var(--uc-secondary-foreground); + --hover-opacity: 1; --hover-filter: 0.8; + --hover-background: var(--uc-secondary-hover); --down-color-rgb: var(--hover-color-rgb); - --down-opacity: 0.12; + --down-opacity: 1; --down-filter: 0.6; + --down-background: var(--uc-secondary); +} +lr-editor-button-control > button, +lr-editor-crop-button-control > button, +lr-editor-filter-control > button, +lr-editor-operation-control > button { + all: unset; position: relative; display: grid; grid-template-columns: var(--l-base-min-width) auto; grid-template-rows: 100%; align-items: center; height: var(--l-base-height); - color: rgba(var(--idle-color-rgb)); + color: var(--color-effect); + opacity: var(--opacity-effect); outline: none; cursor: pointer; transition: var(--l-width-transition); @@ -412,67 +371,73 @@ lr-editor-operation-control { :where(lr-editor-button-control, lr-editor-crop-button-control, lr-editor-filter-control, lr-editor-operation-control) > lr-icon > svg { - width: 20px; - height: 20px; + width: var(--size-icon); + height: var(--size-icon); } -lr-editor-filter-control > lr-icon.original-icon > svg { +lr-editor-filter-control > lr-icon.uc-original-icon > svg { width: 100%; height: 100%; } -lr-editor-button-control.active, -lr-editor-operation-control.active, -lr-editor-crop-button-control.active, -lr-editor-filter-control.active { - --idle-color-rgb: var(--rgb-primary-accent); +lr-editor-button-control.uc-active, +lr-editor-operation-control.uc-active, +lr-editor-crop-button-control.uc-active, +lr-editor-filter-control.uc-active { + --idle-color-rgb: var(--uc-primary); + --idle-background: var(--uc-primary-transparent); + --idle-opacity: 0.9; + --hover-color-rgb: var(--uc-primary); + --hover-background: var(--uc-primary-transparent); + --hover-opacity: 1; } -lr-editor-filter-control.not_active .preview[loaded] { +lr-editor-filter-control.uc-not_active .uc-preview[loaded] { opacity: 1; } -lr-editor-filter-control.active .preview { +lr-editor-filter-control.uc-active .uc-preview { opacity: 0; } -lr-editor-button-control.not_active, -lr-editor-operation-control.not_active, -lr-editor-crop-button-control.not_active, -lr-editor-filter-control.not_active { - --idle-color-rgb: var(--rgb-text-base); +lr-editor-button-control.uc-not_active, +lr-editor-operation-control.uc-not_active, +lr-editor-crop-button-control.uc-not_active, +lr-editor-filter-control.uc-not_active { + --idle-color-rgb: var(--uc-secondary-foreground); } -lr-editor-button-control > .before, -lr-editor-operation-control > .before, -lr-editor-crop-button-control > .before, -lr-editor-filter-control > .before { +lr-editor-button-control > button::before, +lr-editor-operation-control > button::before, +lr-editor-crop-button-control > button::before, +lr-editor-filter-control > button::before { position: absolute; - right: 0px; - left: 0px; + content: ''; + right: 0; + left: 0; z-index: -1; width: 100%; height: 100%; - background-color: rgba(var(--color-effect), var(--opacity-effect)); + background-color: var(--background-effect); border-radius: var(--border-radius-editor); transition: var(--transition-duration-3); } -lr-editor-button-control > .title, -lr-editor-operation-control > .title, -lr-editor-crop-button-control > .title, -lr-editor-filter-control > .title { +lr-editor-button-control > button .uc-title, +lr-editor-operation-control > button .uc-title, +lr-editor-crop-button-control > button .uc-title, +lr-editor-filter-control > button .uc-title { padding-right: var(--cldtr-gap-mid-1); font-size: 0.7em; letter-spacing: 1.004px; text-transform: uppercase; } -lr-editor-filter-control > .preview { +lr-editor-filter-control > button .uc-preview { position: absolute; - right: 0px; - left: 0px; - z-index: 1; + right: 0; + left: 0; + z-index: -1; width: 100%; height: var(--l-base-height); background-repeat: no-repeat; @@ -483,8 +448,8 @@ lr-editor-filter-control > .preview { transition: var(--transition-duration-3); } -lr-editor-filter-control > .original-icon { - color: var(--color-text-base); +lr-editor-filter-control > .uc-original-icon { + color: var(--color-effect); opacity: 0.3; } @@ -499,9 +464,10 @@ lr-editor-image-cropper { opacity: 0; pointer-events: none; touch-action: none; + color: var(--color-crop-guides); } -lr-editor-image-cropper.active_from_editor { +lr-editor-image-cropper.uc-active_from_editor { transform: scale(1) translate(0px, 0px); opacity: 1; transition: @@ -510,7 +476,7 @@ lr-editor-image-cropper.active_from_editor { pointer-events: auto; } -lr-editor-image-cropper.active_from_viewer { +lr-editor-image-cropper.uc-active_from_viewer { transform: scale(1) translate(0px, 0px); opacity: 1; transition: @@ -519,7 +485,7 @@ lr-editor-image-cropper.active_from_viewer { pointer-events: auto; } -lr-editor-image-cropper.inactive_to_editor { +lr-editor-image-cropper.uc-inactive_to_editor { opacity: 0; transition: transform var(--transition-duration-4) cubic-bezier(0.37, 0, 0.63, 1), @@ -527,7 +493,7 @@ lr-editor-image-cropper.inactive_to_editor { pointer-events: none; } -lr-editor-image-cropper > .canvas { +lr-editor-image-cropper > .uc-canvas { position: absolute; top: 0px; left: 0px; @@ -546,7 +512,7 @@ lr-editor-image-fader { height: 100%; } -lr-editor-image-fader.active_from_viewer { +lr-editor-image-fader.uc-active_from_viewer { z-index: 3; transform: scale(1); opacity: 1; @@ -556,7 +522,7 @@ lr-editor-image-fader.active_from_viewer { pointer-events: auto; } -lr-editor-image-fader.active_from_cropper { +lr-editor-image-fader.uc-active_from_cropper { z-index: 3; transform: scale(1); opacity: 1; @@ -566,7 +532,7 @@ lr-editor-image-fader.active_from_cropper { pointer-events: auto; } -lr-editor-image-fader.inactive_to_cropper { +lr-editor-image-fader.uc-inactive_to_cropper { z-index: 3; transform: scale(1); opacity: 0; @@ -576,7 +542,7 @@ lr-editor-image-fader.inactive_to_cropper { pointer-events: none; } -lr-editor-image-fader .fader-image { +lr-editor-image-fader .uc-fader-image { position: absolute; top: 0px; left: 0px; @@ -589,7 +555,7 @@ lr-editor-image-fader .fader-image { content-visibility: auto; } -lr-editor-image-fader .fader-image--preview { +lr-editor-image-fader .uc-fader-image--preview { background-color: var(--color-image-background); border-top-left-radius: var(--border-radius-base); border-top-right-radius: var(--border-radius-base); @@ -611,7 +577,7 @@ lr-editor-slider { align-items: center; justify-content: center; width: 100%; - height: 66px; + height: var(--size-panel-heading); } lr-editor-toolbar { @@ -623,27 +589,27 @@ lr-editor-toolbar { @media only screen and (max-width: 600px) { lr-editor-toolbar { --l-tab-gap: var(--cldtr-gap-mid-1); - --l-slider-padding: var(--cldtr-gap-min); - --l-controls-padding: var(--cldtr-gap-min); + --l-slider-padding: var(--uc-padding); + --l-controls-padding: var(--uc-padding); } } @media only screen and (min-width: 601px) { lr-editor-toolbar { - --l-tab-gap: calc(var(--cldtr-gap-mid-1) + var(--cldtr-gap-max)); - --l-slider-padding: var(--cldtr-gap-mid-1); - --l-controls-padding: var(--cldtr-gap-mid-1); + --l-tab-gap: var(--cldtr-gap-max); + --l-slider-padding: var(--uc-padding); + --l-controls-padding: var(--uc-padding); } } -lr-editor-toolbar > .toolbar-container { +lr-editor-toolbar > .uc-toolbar-container { position: relative; width: 100%; height: 100%; overflow: hidden; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar { position: absolute; display: grid; grid-template-rows: 1fr 1fr; @@ -656,25 +622,25 @@ lr-editor-toolbar > .toolbar-container > .sub-toolbar { visibility var(--transition-duration-3) ease-in-out; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar.sub-toolbar--visible { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar.uc-sub-toolbar--visible { transform: translateY(0px); opacity: 1; pointer-events: auto; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar.sub-toolbar--top-hidden { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar.uc-sub-toolbar--top-hidden { transform: translateY(100%); opacity: 0; pointer-events: none; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar.sub-toolbar--bottom-hidden { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar.uc-sub-toolbar--bottom-hidden { transform: translateY(-100%); opacity: 0; pointer-events: none; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar > .uc-controls-row { display: flex; align-items: center; justify-content: space-between; @@ -682,7 +648,7 @@ lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row { padding-left: var(--l-controls-padding); } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar > .uc-controls-row > .uc-tab-toggles { position: relative; display: grid; grid-auto-flow: column; @@ -691,22 +657,27 @@ lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-tog height: 100%; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles > .tab-toggles_indicator { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-controls-row + > .uc-tab-toggles + > .uc-tab-toggles_indicator { position: absolute; bottom: 0px; left: 0px; width: var(--size-touch-area); height: 2px; - background-color: var(--color-primary-accent); + background-color: var(--uc-secondary-foreground); transform: translateX(0px); transition: transform var(--transition-duration-3); } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar > .uc-tab-content-row { position: relative; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-content { +lr-editor-toolbar > .uc-toolbar-container > .uc-sub-toolbar > .uc-tab-content-row > .uc-tab-content { position: absolute; top: 0px; left: 0px; @@ -718,75 +689,113 @@ lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab- content-visibility: auto; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-content.tab-content--visible { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-tab-content-row + > .uc-tab-content.uc-tab-content--visible { opacity: 1; pointer-events: auto; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-content.tab-content--hidden { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-tab-content-row + > .uc-tab-content.uc-tab-content--hidden { opacity: 0; pointer-events: none; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles > .tab-toggle.tab-toggle--visible { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-controls-row + > .uc-tab-toggles + > .uc-tab-toggle.uc-tab-toggle--visible { display: contents; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles > .tab-toggle.tab-toggle--hidden { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-controls-row + > .uc-tab-toggles + > .uc-tab-toggle.uc-tab-toggle--hidden { display: none; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles.tab-toggles--hidden { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-controls-row + > .uc-tab-toggles.uc-tab-toggles--hidden { display: none; } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .controls-row > .tab-toggles > .tab-toggle > lr-btn-ui { - width: 40px; +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-controls-row + > .uc-tab-toggles + > .uc-tab-toggle + > lr-btn-ui { + width: var(--uc-button-size); } lr-editor-toolbar - > .toolbar-container - > .sub-toolbar - > .controls-row - > .tab-toggles - > .tab-toggle + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-controls-row + > .uc-tab-toggles + > .uc-tab-toggle > lr-btn-ui > lr-icon > svg { - width: 100%; - height: 100%; + width: var(--size-icon); + height: var(--size-icon); } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-content .controls-list_align { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-tab-content-row + > .uc-tab-content + .uc-controls-list_align { display: grid; grid-template-areas: '. inner .'; grid-template-columns: 1fr auto 1fr; box-sizing: border-box; min-width: 100%; - padding-left: var(--cldtr-gap-max); + padding-left: var(--uc-padding); } -lr-editor-toolbar > .toolbar-container > .sub-toolbar > .tab-content-row > .tab-content .controls-list_inner { +lr-editor-toolbar + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-tab-content-row + > .uc-tab-content + .uc-controls-list_inner { display: grid; grid-area: inner; grid-auto-flow: column; - grid-gap: calc((var(--cldtr-gap-min) - 1px) * 3); + gap: 6px; } lr-editor-toolbar - > .toolbar-container - > .sub-toolbar - > .tab-content-row - > .tab-content - .controls-list_inner:last-child { - padding-right: var(--cldtr-gap-max); + > .uc-toolbar-container + > .uc-sub-toolbar + > .uc-tab-content-row + > .uc-tab-content + .uc-controls-list_inner:last-child { + padding-right: var(--uc-padding); } -lr-editor-toolbar .controls-list_last-item { +lr-editor-toolbar .uc-controls-list_last-item { margin-right: var(--cldtr-gap-max); } -lr-editor-toolbar .info-tooltip_container { +lr-editor-toolbar .uc-info-tooltip_container { position: absolute; display: flex; align-items: flex-start; @@ -795,7 +804,7 @@ lr-editor-toolbar .info-tooltip_container { height: 100%; } -lr-editor-toolbar .info-tooltip_wrapper { +lr-editor-toolbar .uc-info-tooltip_wrapper { position: absolute; top: calc(-100% - var(--cldtr-gap-mid-2)); display: flex; @@ -805,12 +814,9 @@ lr-editor-toolbar .info-tooltip_wrapper { pointer-events: none; } -lr-editor-toolbar .info-tooltip { +lr-editor-toolbar .uc-info-tooltip { z-index: 3; - padding-top: calc(var(--cldtr-gap-min) / 2); - padding-right: var(--cldtr-gap-min); - padding-bottom: calc(var(--cldtr-gap-min) / 2); - padding-left: var(--cldtr-gap-min); + padding: 3px 6px; color: var(--color-text-base); font-size: 0.7em; letter-spacing: 1px; @@ -822,29 +828,40 @@ lr-editor-toolbar .info-tooltip { transition: var(--transition-duration-3); } -lr-editor-toolbar .info-tooltip_visible { +lr-editor-toolbar .uc-info-tooltip_visible { transform: translateY(0px); opacity: 1; } -lr-editor-toolbar .slider { +lr-editor-toolbar .uc-slider { padding-right: var(--l-slider-padding); padding-left: var(--l-slider-padding); } -lr-btn-ui { +lr-btn-ui > button { --filter-effect: var(--idle-brightness); --opacity-effect: var(--idle-opacity); --color-effect: var(--idle-color-rgb); - --l-transition-effect: var(--css-transition, color var(--transition-duration-2), filter var(--transition-duration-2)); - + --background-effect: var(--idle-background); + --l-transition-effect: var( + --css-transition, + color var(--transition-duration-2), + background-color var(--transition-duration-2), + filter var(--transition-duration-2) + ); + + all: unset; display: inline-flex; align-items: center; box-sizing: var(--css-box-sizing, border-box); - height: var(--css-height, var(--size-touch-area)); - padding-right: var(--css-padding-right, var(--cldtr-gap-mid-1)); - padding-left: var(--css-padding-left, var(--cldtr-gap-mid-1)); - color: rgba(var(--color-effect), var(--opacity-effect)); + height: var(--css-height, var(--uc-button-size)); + padding-right: var(--css-padding-right, 14px); + padding-left: var(--css-padding-left, 14px); + font-size: 1em; + color: var(--color-effect); + background-color: var(--background-effect); + border-radius: var(--uc-radius); + opacity: var(--opacity-effect); outline: none; cursor: pointer; filter: brightness(var(--filter-effect)); @@ -852,82 +869,160 @@ lr-btn-ui { user-select: none; } -lr-btn-ui .text { +lr-btn-ui .uc-text { white-space: nowrap; } -lr-btn-ui .icon { +lr-btn-ui .uc-icon { display: flex; align-items: center; justify-content: center; - color: rgba(var(--color-effect), var(--opacity-effect)); + color: var(--color-effect); filter: brightness(var(--filter-effect)); transition: var(--l-transition-effect); } -lr-btn-ui .icon_left { +lr-btn-ui .uc-icon_left { margin-right: var(--cldtr-gap-mid-1); margin-left: 0px; } -lr-btn-ui .icon_right { +lr-btn-ui .uc-icon_right { margin-right: 0px; margin-left: var(--cldtr-gap-mid-1); } -lr-btn-ui .icon_single { +lr-btn-ui .uc-icon_single { margin-right: 0px; margin-left: 0px; } -lr-btn-ui .icon_hidden { +lr-btn-ui .uc-icon_hidden { display: none; margin: 0; } -lr-btn-ui.primary { - --idle-color-rgb: var(--rgb-primary-accent); +lr-btn-ui.uc-primary > button { + --idle-color-rgb: var(--uc-primary-foreground); --idle-brightness: 1; - --idle-opacity: 0.6; - --hover-color-rgb: var(--idle-color-rgb); + --idle-opacity: 1; + --idle-background: var(--uc-primary); + --hover-color-rgb: var(--uc-primary-foreground); --hover-brightness: 1; --hover-opacity: 1; - --down-color-rgb: var(--hover-color-rgb); + --hover-background: var(--uc-primary-hover); + --down-color-rgb: var(--uc-primary-foreground); --down-brightness: 0.75; --down-opacity: 1; - --active-color-rgb: var(--rgb-primary-accent); + --down-background: var(--uc-primary); + --active-color-rgb: var(--uc-primary-foreground); --active-brightness: 1; --active-opacity: 1; + --active-background: var(--uc-primary); } -lr-btn-ui.boring { - --idle-color-rgb: var(--rgb-text-base); +lr-btn-ui.uc-primary-icon > button { + --idle-color-rgb: var(--uc-primary); --idle-brightness: 1; - --idle-opacity: 0.6; - --hover-color-rgb: var(--rgb-text-base); + --idle-opacity: 1; + --idle-background: transparent; + --hover-color-rgb: var(--uc-primary); --hover-brightness: 1; --hover-opacity: 1; - --down-color-rgb: var(--hover-color-rgb); + --hover-background: var(--uc-primary-transparent); + --down-color-rgb: var(--uc-primary); + --down-brightness: 0.75; + --down-opacity: 1; + --down-background: var(--uc-primary-transparent); + --active-color-rgb: var(--uc-primary-foreground); + --active-brightness: 1; + --active-opacity: 1; + --active-background: var(--uc-primary); + + padding: 0; + width: var(--uc-button-size); +} + +lr-btn-ui.uc-secondary > button { + --idle-color-rgb: var(--uc-secondary-foreground); + --idle-brightness: 1; + --idle-opacity: 1; + --idle-background: var(--uc-secondary); + --hover-color-rgb: var(--uc-secondary-foreground); + --hover-brightness: 1; + --hover-opacity: 1; + --hover-background: var(--uc-secondary-hover); + --down-color-rgb: var(--uc-secondary-foreground); --down-brightness: 1; --down-opacity: 1; - --active-color-rgb: var(--rgb-primary-accent); + --down-background: var(--uc-secondary-hover); + --active-color-rgb: var(--uc-secondary-foreground); --active-brightness: 1; --active-opacity: 1; + --active-background: transparent; } -lr-btn-ui.default { - --idle-color-rgb: var(--rgb-text-base); +lr-btn-ui.uc-secondary-icon > button { + --idle-color-rgb: var(--uc-secondary-foreground); --idle-brightness: 1; - --idle-opacity: 0.6; - --hover-color-rgb: var(--rgb-primary-accent); + --idle-opacity: 1; + --idle-background: transparent; + --hover-color-rgb: var(--uc-secondary-foreground); --hover-brightness: 1; --hover-opacity: 1; - --down-color-rgb: var(--hover-color-rgb); + --hover-background: var(--uc-secondary); + --down-color-rgb: var(--uc-secondary-foreground); + --down-brightness: 1; + --down-opacity: 1; + --down-background: var(--uc-secondary); + --active-color-rgb: var(--uc-secondary-foreground); + --active-brightness: 1; + --active-opacity: 1; + --active-background: transparent; + + padding: 0; + width: var(--uc-button-size); +} + +lr-btn-ui.uc-tab > button { + --idle-color-rgb: var(--uc-secondary-foreground); + --idle-brightness: 1; + --idle-opacity: 1; + --idle-background: transparent; + --hover-color-rgb: var(--uc-secondary-foreground); + --hover-brightness: 1; + --hover-opacity: 1; + --hover-background: var(--uc-secondary); + --down-color-rgb: var(--uc-secondary-foreground); + --down-brightness: 1; + --down-opacity: 1; + --down-background: var(--uc-secondary); + --active-color-rgb: var(--uc-secondary-foreground); + --active-brightness: 1; + --active-opacity: 1; + --active-background: transparent; + + padding: 0; + width: var(--uc-button-size); +} + +lr-btn-ui.uc-default > button { + --idle-color-rgb: var(--uc-secondary-foreground); + --idle-brightness: 1; + --idle-opacity: 1; + --idle-background: var(--uc-secondary); + --hover-color-rgb: var(--uc-secondary-foreground); + --hover-brightness: 1; + --hover-opacity: 1; + --hover-background: var(--uc-secondary-hover); + --down-color-rgb: var(--uc-secondary-foreground); --down-brightness: 0.75; --down-opacity: 1; - --active-color-rgb: var(--rgb-primary-accent); + --down-background: var(--uc-secondary); + --active-color-rgb: var(--uc-primary); --active-brightness: 1; --active-opacity: 1; + --active-background: var(--uc-primary-transparent); } lr-line-loader-ui { @@ -940,16 +1035,16 @@ lr-line-loader-ui { opacity: 0.5; } -lr-line-loader-ui .inner { +lr-line-loader-ui .uc-inner { width: 25%; max-width: 200px; height: 100%; } -lr-line-loader-ui .line { +lr-line-loader-ui .uc-line { width: 100%; height: 100%; - background-color: var(--color-primary-accent); + background-color: var(--uc-primary); transform: translateX(-101%); transition: transform 1s; } @@ -958,11 +1053,11 @@ lr-slider-ui { --l-thumb-size: 24px; --l-zero-dot-size: 5px; --l-zero-dot-offset: 2px; - --idle-color-rgb: var(--rgb-text-base); - --hover-color-rgb: var(--rgb-primary-accent); - --down-color-rgb: var(--rgb-primary-accent); + --idle-color-rgb: var(--uc-foreground); + --hover-color-rgb: var(--uc-primary); + --down-color-rgb: var(--uc-primary); --color-effect: var(--idle-color-rgb); - --l-color: rgb(var(--color-effect)); + --l-color: var(--color-effect); position: relative; display: flex; @@ -972,7 +1067,7 @@ lr-slider-ui { height: calc(var(--l-thumb-size) + (var(--l-zero-dot-size) + var(--l-zero-dot-offset)) * 2); } -lr-slider-ui .thumb { +lr-slider-ui .uc-thumb { position: absolute; left: 0px; width: var(--l-thumb-size); @@ -981,10 +1076,12 @@ lr-slider-ui .thumb { border-radius: 50%; transform: translateX(0px); opacity: 1; - transition: opacity var(--transition-duration-2); + transition: + opacity var(--transition-duration-2), + background-color var(--transition-duration-2); } -lr-slider-ui .steps { +lr-slider-ui .uc-steps { position: absolute; display: flex; align-items: center; @@ -996,23 +1093,23 @@ lr-slider-ui .steps { padding-left: calc(var(--l-thumb-size) / 2); } -lr-slider-ui .border-step { +lr-slider-ui .uc-border-step { width: 0px; height: 10px; border-right: 1px solid var(--l-color); opacity: 0.6; - transition: var(--transition-duration-2); + transition: border-color var(--transition-duration-2); } -lr-slider-ui .minor-step { +lr-slider-ui .uc-minor-step { width: 0px; height: 4px; border-right: 1px solid var(--l-color); opacity: 0.2; - transition: var(--transition-duration-2); + transition: border-color var(--transition-duration-2); } -lr-slider-ui .zero-dot { +lr-slider-ui .uc-zero-dot { position: absolute; top: calc(100% - var(--l-zero-dot-offset) * 2); left: calc(var(--l-thumb-size) / 2 - var(--l-zero-dot-size) / 2); @@ -1024,7 +1121,7 @@ lr-slider-ui .zero-dot { transition: var(--transition-duration-3); } -lr-slider-ui .input { +lr-slider-ui .uc-input { position: absolute; width: calc(100% - 10px); height: 100%; @@ -1033,35 +1130,28 @@ lr-slider-ui .input { opacity: 0; } -lr-presence-toggle.transition { +lr-presence-toggle.uc-transition { transition: opacity var(--transition-duration-3), visibility var(--transition-duration-3); } -lr-presence-toggle.visible { +lr-presence-toggle.uc-visible { opacity: 1; pointer-events: inherit; } -lr-presence-toggle.hidden { +lr-presence-toggle.uc-hidden { opacity: 0; pointer-events: none; } -/* TODO: remove ctx-provider */ -/* stylelint-disable-next-line plugin/stylelint-force-app-name-prefix */ -ctx-provider { - --color-text-base: black; - --color-primary-accent: blue; +lr-presence-toggle.uc-initial { + display: none !important; + transition: none !important; +} - display: flex; - align-items: center; - justify-content: center; - width: 190px; - height: 40px; - padding-right: 10px; - padding-left: 10px; - background-color: #f5f5f5; - border-radius: 3px; +[lr-cloud-image-editor] [role='button']:focus-visible, +[lr-cloud-image-editor] button:focus-visible { + outline: 1px auto -webkit-focus-ring-color; } diff --git a/blocks/CloudImageEditor/src/css/icons.css b/blocks/CloudImageEditor/src/css/icons.css index bedf8eb19..7fa07e5fa 100644 --- a/blocks/CloudImageEditor/src/css/icons.css +++ b/blocks/CloudImageEditor/src/css/icons.css @@ -1,7 +1,12 @@ -.lr-cloud-image-editor lr-icon { +:where([lr-cloud-image-editor]) lr-icon { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; } + +:where([lr-cloud-image-editor]) lr-icon svg { + width: calc(var(--uc-button-size) / 2); + height: calc(var(--uc-button-size) / 2); +} diff --git a/blocks/CloudImageEditor/src/elements/button/LrBtnUi.js b/blocks/CloudImageEditor/src/elements/button/LrBtnUi.js index 993954036..ca09ce851 100644 --- a/blocks/CloudImageEditor/src/elements/button/LrBtnUi.js +++ b/blocks/CloudImageEditor/src/elements/button/LrBtnUi.js @@ -15,6 +15,8 @@ export class LrBtnUi extends Block { icon: '', iconCss: this._iconCss(), theme: null, + 'aria-role': '', + 'aria-controls': '', }; this.defineAccessor('active', (active) => { @@ -27,11 +29,11 @@ export class LrBtnUi extends Block { } _iconCss() { - return classNames('icon', { - icon_left: !this._iconReversed, - icon_right: this._iconReversed, - icon_hidden: this._iconHidden, - icon_single: this._iconSingle, + return classNames('uc-icon', { + 'uc-icon_left': !this._iconReversed, + 'uc-icon_right': this._iconReversed, + 'uc-icon_hidden': this._iconHidden, + 'uc-icon_single': this._iconSingle, }); } @@ -46,7 +48,7 @@ export class LrBtnUi extends Block { this.sub('theme', (theme) => { if (theme !== 'custom') { - this.className = theme; + this.className = `uc-${theme}`; } }); @@ -54,13 +56,17 @@ export class LrBtnUi extends Block { this._iconSingle = false; }); - this.setAttribute('role', 'button'); - if (this.tabIndex === -1) { - this.tabIndex = 0; - } if (!this.hasAttribute('theme')) { this.setAttribute('theme', 'default'); } + + this.defineAccessor('aria-role', (value) => { + this.$['aria-role'] = value || ''; + }); + + this.defineAccessor('aria-controls', (value) => { + this.$['aria-controls'] = value || ''; + }); } set reverse(val) { @@ -73,9 +79,12 @@ export class LrBtnUi extends Block { } } } + LrBtnUi.bindAttributes({ text: 'text', icon: 'icon', reverse: 'reverse', theme: 'theme' }); LrBtnUi.template = /* HTML */ ` - -
{{text}}
+ `; diff --git a/blocks/CloudImageEditor/src/elements/line-loader/LineLoaderUi.js b/blocks/CloudImageEditor/src/elements/line-loader/LineLoaderUi.js index e66cadfb3..6cb45e6d4 100644 --- a/blocks/CloudImageEditor/src/elements/line-loader/LineLoaderUi.js +++ b/blocks/CloudImageEditor/src/elements/line-loader/LineLoaderUi.js @@ -46,7 +46,7 @@ export class LineLoaderUi extends Block { } LineLoaderUi.template = /* HTML */ ` -
-
+
+
`; diff --git a/blocks/CloudImageEditor/src/elements/presence-toggle/PresenceToggle.js b/blocks/CloudImageEditor/src/elements/presence-toggle/PresenceToggle.js index 35b16e57c..b53453378 100644 --- a/blocks/CloudImageEditor/src/elements/presence-toggle/PresenceToggle.js +++ b/blocks/CloudImageEditor/src/elements/presence-toggle/PresenceToggle.js @@ -10,9 +10,9 @@ import { Block } from '../../../../../abstract/Block.js'; /** @type {Style} */ const DEFAULT_STYLE = { - transition: 'transition', - visible: 'visible', - hidden: 'hidden', + transition: 'uc-transition', + visible: 'uc-visible', + hidden: 'uc-hidden', }; export class PresenceToggle extends Block { @@ -55,14 +55,17 @@ export class PresenceToggle extends Block { initCallback() { super.initCallback(); - this.setAttribute('hidden', ''); + + this.classList.toggle('uc-initial', true); if (!this._externalTransitions) { this.classList.add(DEFAULT_STYLE.transition); } this._handleVisible(); - setTimeout(() => this.removeAttribute('hidden'), 0); + setTimeout(() => { + this.classList.toggle('uc-initial', false); + }, 0); } } -PresenceToggle.template = /* HTML */ ` `; +PresenceToggle.template = /* HTML */ ` `; diff --git a/blocks/CloudImageEditor/src/elements/slider/SliderUi.js b/blocks/CloudImageEditor/src/elements/slider/SliderUi.js index bab0180bc..4548c34b2 100644 --- a/blocks/CloudImageEditor/src/elements/slider/SliderUi.js +++ b/blocks/CloudImageEditor/src/elements/slider/SliderUi.js @@ -167,13 +167,12 @@ export class SliderUi extends Block { } } SliderUi.template = /* HTML */ ` -
-
+
+
`; diff --git a/blocks/CloudImageEditor/src/icons/closeMax.svg b/blocks/CloudImageEditor/src/icons/closeMax.svg index 3e621f4de..88e57184c 100644 --- a/blocks/CloudImageEditor/src/icons/closeMax.svg +++ b/blocks/CloudImageEditor/src/icons/closeMax.svg @@ -1,3 +1,3 @@ - - + + diff --git a/blocks/CloudImageEditor/src/icons/done.svg b/blocks/CloudImageEditor/src/icons/done.svg index 87a628a61..78ff9b54e 100644 --- a/blocks/CloudImageEditor/src/icons/done.svg +++ b/blocks/CloudImageEditor/src/icons/done.svg @@ -1,3 +1,3 @@ - - + + diff --git a/blocks/CloudImageEditor/src/icons/edit-file.svg b/blocks/CloudImageEditor/src/icons/edit-file.svg new file mode 100644 index 000000000..c88b71952 --- /dev/null +++ b/blocks/CloudImageEditor/src/icons/edit-file.svg @@ -0,0 +1,3 @@ + + + diff --git a/blocks/CloudImageEditor/src/icons/original.svg b/blocks/CloudImageEditor/src/icons/original.svg index abe71893a..906db13f9 100644 --- a/blocks/CloudImageEditor/src/icons/original.svg +++ b/blocks/CloudImageEditor/src/icons/original.svg @@ -1,3 +1,3 @@ - + diff --git a/blocks/CloudImageEditor/src/svg-sprite.js b/blocks/CloudImageEditor/src/svg-sprite.js index 89542dbfc..832ac3c9a 100644 --- a/blocks/CloudImageEditor/src/svg-sprite.js +++ b/blocks/CloudImageEditor/src/svg-sprite.js @@ -1 +1 @@ -export default ""; +export default ""; diff --git a/blocks/CloudImageEditor/src/template.js b/blocks/CloudImageEditor/src/template.js index 0d69e4cba..6f3e042e5 100644 --- a/blocks/CloudImageEditor/src/template.js +++ b/blocks/CloudImageEditor/src/template.js @@ -3,32 +3,32 @@ import svgIconsSprite from './svg-sprite.js'; export const TEMPLATE = /* HTML */ ` ${svgIconsSprite} -
- -
-
+
+ +
+
-
Network error
+
Network error
-