@uploadcare/file-uploader
Version:
Building blocks for Uploadcare products integration
229 lines (198 loc) • 6.4 kB
JavaScript
// @ts-check
import { Block } from '../../../abstract/Block.js';
import {
createCdnUrl,
createCdnUrlModifiers,
createOriginalUrl,
extractOperations,
extractUuid,
} from '../../../utils/cdn-utils.js';
import { TRANSPARENT_PIXEL_SRC } from '../../../utils/transparentPixelSrc.js';
import { debounce } from '../../utils/debounce.js';
import { classNames } from './lib/classNames.js';
import { parseCropPreset } from './lib/parseCropPreset.js';
import { parseTabs } from './lib/parseTabs.js';
import { operationsToTransformations, transformationsToOperations } from './lib/transformationUtils.js';
import { initState } from './state.js';
import { TEMPLATE } from './template.js';
import { TabId } from './toolbar-constants.js';
export class CloudImageEditorBlock extends Block {
ctxOwner = true;
static styleAttrs = ['uc-cloud-image-editor'];
constructor() {
super();
this.init$ = {
...this.init$,
...initState(this),
};
}
/** @private */
_debouncedShowLoader = debounce(this._showLoader.bind(this), 300);
/**
* @private
* @param {boolean} show
*/
_showLoader(show) {
this.$.showLoader = show;
}
/**
* To proper work, we need non-zero size the element. So, we'll wait for it.
*
* @private
* @returns {Promise<void>}
*/
_waitForSize() {
const TIMEOUT = 3000;
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('[cloud-image-editor] timeout waiting for non-zero container size'));
}, TIMEOUT);
const resizeObserver = new ResizeObserver(([element]) => {
if (element.contentRect.width > 0 && element.contentRect.height > 0) {
resolve();
clearTimeout(timeoutId);
resizeObserver.disconnect();
}
});
resizeObserver.observe(this);
});
}
initCallback() {
super.initCallback();
this.$['*faderEl'] = this.ref['fader-el'];
this.$['*cropperEl'] = this.ref['cropper-el'];
this.$['*imgContainerEl'] = this.ref['img-container-el'];
this.initEditor();
}
async updateImage() {
if (!this.isConnected) {
return;
}
await this._waitForSize();
if (this.$.cdnUrl) {
const uuid = extractUuid(this.$.cdnUrl);
const originalUrl = createOriginalUrl(this.$.cdnUrl, uuid);
if (originalUrl === this.$['*originalUrl']) {
return;
}
this.$['*originalUrl'] = originalUrl;
const operations = extractOperations(this.$.cdnUrl);
const transformations = operationsToTransformations(operations);
this.$['*editorTransformations'] = transformations;
} else if (this.$.uuid) {
const originalUrl = createOriginalUrl(this.cfg.cdnCname, this.$.uuid);
if (originalUrl === this.$['*originalUrl']) {
return;
}
this.$['*originalUrl'] = originalUrl;
this.$['*editorTransformations'] = {};
} else {
throw new Error('No UUID nor CDN URL provided');
}
if (this.$['*tabId'] === TabId.CROP) {
this.$['*cropperEl'].deactivate({ reset: true });
} else {
this.$['*faderEl'].deactivate();
}
try {
const cdnUrl = await this.proxyUrl(createCdnUrl(this.$['*originalUrl'], createCdnUrlModifiers('json')));
const json = await fetch(cdnUrl).then((response) => response.json());
const { width, height } = /** @type {{ width: number; height: number }} */ (json);
this.$['*imageSize'] = { width, height };
if (this.$['*tabId'] === TabId.CROP) {
this.$['*cropperEl'].activate(this.$['*imageSize']);
} else {
this.$['*faderEl'].activate({ url: this.$['*originalUrl'] });
}
} catch (err) {
if (err) {
console.error('Failed to load image info', err);
}
}
}
async initEditor() {
try {
await this._waitForSize();
} catch (err) {
if (this.isConnected) {
// @ts-ignore TODO: fix this
console.error(err.message);
}
return;
}
this.ref['img-el'].addEventListener('load', () => {
this._imgLoading = false;
this._debouncedShowLoader(false);
if (this.$.src !== TRANSPARENT_PIXEL_SRC) {
this.$['*networkProblems'] = false;
}
});
this.ref['img-el'].addEventListener('error', () => {
this._imgLoading = false;
this._debouncedShowLoader(false);
this.$['*networkProblems'] = true;
});
this.sub('src', (src) => {
let el = this.ref['img-el'];
if (el.src !== src) {
this._imgLoading = true;
el.src = src || TRANSPARENT_PIXEL_SRC;
}
});
this.sub('cropPreset', (val) => {
this.$['*cropPresetList'] = parseCropPreset(val);
});
this.sub(
'tabs',
/** @param {string} val */ (val) => {
this.$['*tabList'] = parseTabs(val);
},
);
this.sub('*tabId', (tabId) => {
this.ref['img-el'].className = classNames('uc-image', {
'uc-image_hidden_to_cropper': tabId === TabId.CROP,
'uc-image_hidden_effects': tabId !== TabId.CROP,
});
});
this.classList.add('uc-editor_ON');
this.sub('*networkProblems', (networkProblems) => {
this.$['presence.networkProblems'] = networkProblems;
this.$['presence.modalCaption'] = !networkProblems;
});
this.sub(
'*editorTransformations',
(transformations) => {
if (Object.keys(transformations).length === 0) {
return;
}
let originalUrl = this.$['*originalUrl'];
let cdnUrlModifiers = createCdnUrlModifiers(transformationsToOperations(transformations), 'preview');
let cdnUrl = createCdnUrl(originalUrl, cdnUrlModifiers);
/** @type {import('./types.js').ApplyResult} */
let eventData = {
originalUrl,
cdnUrlModifiers,
cdnUrl,
transformations,
};
this.dispatchEvent(
new CustomEvent('change', {
detail: eventData,
bubbles: true,
composed: true,
}),
);
},
false,
);
this.sub('uuid', (val) => val && this.updateImage());
this.sub('cdnUrl', (val) => val && this.updateImage());
}
}
CloudImageEditorBlock.template = TEMPLATE;
CloudImageEditorBlock.bindAttributes({
uuid: 'uuid',
'cdn-url': 'cdnUrl',
'crop-preset': 'cropPreset',
tabs: 'tabs',
});