From 10ec5890cb0d3a3760c295117456db1cc0ddc9b5 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 22 Sep 2022 13:20:11 +0400 Subject: [PATCH] Migrate Asset Manager to TS (#4604) * Update assets config * Update index TS * Up TS * Init TS update for assets index * Move assets models to TS * Update AssetView * Update AssetImageView * Update AssetsView * Update FileUploaderView * Update FileUploader * Fix asset tests * Use data-input attribute for events in FileUploader * Up * Update JSDoc config link * Up docs --- docs/api.js | 4 +- docs/api/assets.md | 6 +- index.d.ts | 136 ++++- src/abstract/Module.ts | 6 +- src/asset_manager/config/config.js | 110 ---- src/asset_manager/config/config.ts | 156 ++++++ src/asset_manager/index.js | 472 ------------------ src/asset_manager/index.ts | 463 +++++++++++++++++ .../model/{Asset.js => Asset.ts} | 8 +- .../model/{AssetImage.js => AssetImage.ts} | 0 .../model/{Assets.js => Assets.ts} | 7 +- .../{AssetImageView.js => AssetImageView.ts} | 11 +- .../view/{AssetView.js => AssetView.ts} | 40 +- .../view/{AssetsView.js => AssetsView.ts} | 49 +- .../view/{FileUploader.js => FileUploader.ts} | 290 ++++++----- test/specs/asset_manager/index.js | 3 +- 16 files changed, 996 insertions(+), 765 deletions(-) delete mode 100644 src/asset_manager/config/config.js create mode 100644 src/asset_manager/config/config.ts delete mode 100644 src/asset_manager/index.js create mode 100644 src/asset_manager/index.ts rename src/asset_manager/model/{Asset.js => Asset.ts} (94%) rename src/asset_manager/model/{AssetImage.js => AssetImage.ts} (100%) rename src/asset_manager/model/{Assets.js => Assets.ts} (70%) rename src/asset_manager/view/{AssetImageView.js => AssetImageView.ts} (89%) rename src/asset_manager/view/{AssetView.js => AssetView.ts} (57%) rename src/asset_manager/view/{AssetsView.js => AssetsView.ts} (76%) rename src/asset_manager/view/{FileUploader.js => FileUploader.ts} (53%) diff --git a/docs/api.js b/docs/api.js index 48b0f3252..889e91e27 100644 --- a/docs/api.js +++ b/docs/api.js @@ -12,8 +12,8 @@ async function generateDocs () { await Promise.all([ ['editor/index.ts', 'editor.md'], - ['asset_manager/index.js', 'assets.md'], - ['asset_manager/model/Asset.js', 'asset.md'], + ['asset_manager/index.ts', 'assets.md'], + ['asset_manager/model/Asset.ts', 'asset.md'], ['block_manager/index.js', 'block_manager.md'], ['block_manager/model/Block.js', 'block.md'], ['commands/index.js', 'commands.md'], diff --git a/docs/api/assets.md b/docs/api/assets.md index 6b86c53e1..fc07e7e74 100644 --- a/docs/api/assets.md +++ b/docs/api/assets.md @@ -1,6 +1,6 @@ -## AssetManager +## Assets You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1] @@ -160,7 +160,7 @@ Remove asset ### Parameters * `asset` **([String][13] | [Asset])** Asset or asset URL -* `opts` +* `opts` **Record<[string][13], any>?** ### Examples @@ -179,7 +179,7 @@ Return the Asset Manager Container Returns **[HTMLElement][16]** -[1]: https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.js +[1]: https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.ts [2]: #open diff --git a/index.d.ts b/index.d.ts index 678434e9b..f2fa4d225 100644 --- a/index.d.ts +++ b/index.d.ts @@ -258,31 +258,135 @@ declare namespace grapesjs { } interface AssetManagerConfig { - assets?: Array; + /** + * Default assets. + * @example + * [ + * 'https://...image1.png', + * 'https://...image2.png', + * {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1} + * ] + */ + assets?: (string | Record)[]; + /** + * Content to add where there is no assets to show. + * @default '' + * @example 'No assets here, drag to upload' + */ noAssets?: string; + /** + * Style prefix + * @default 'am-' + */ stylePrefix?: string; - upload?: boolean; + /** + * Upload endpoint, set `false` to disable upload. + * @example 'https://endpoint/upload/assets' + */ + upload?: false | string; + /** + * The name used in POST to pass uploaded files. + * @default 'files' + */ uploadName?: string; - headers?: object; - params?: object; + /** + * Custom headers to pass with the upload request. + * @default {} + */ + headers?: Record; + /** + * Custom parameters to pass with the upload request, eg. csrf token. + * @default {} + */ + params?: Record; + /** + * The credentials setting for the upload request, eg. 'include', 'omit'. + * @default 'include' + */ credentials?: RequestCredentials; + /** + * Allow uploading multiple files per request. If disabled filename will not have '[]' appended. + * @default true + */ multiUpload?: boolean; + /** + * If true, tries to add automatically uploaded assets. To make it work the server should respond with a JSON containing assets in a data key, eg: + * { data: [ 'https://.../image.png', {src: 'https://.../image2.png'} ] + * @default true + */ autoAdd?: boolean; - uploadText?: string; - addBtnText?: string; - customFetch?: Function; - uploadFile?: Function; + /** + * To upload your assets, the module uses Fetch API. With this option you can overwrite it with your own logic. The custom function should return a Promise. + * @example + * customFetch: (url, options) => axios(url, { data: options.body }), + */ + customFetch?: (url: string, options: Record) => Promise; + /** + * Custom uploadFile function. + * Differently from the `customFetch` option, this gives a total control over the uploading process, but you also have to emit all `asset:upload:*` events b + * y yourself (if you need to use them somewhere). + * @example + * uploadFile: (ev) => { + * const files = ev.dataTransfer ? ev.dataTransfer.files : ev.target.files; + * // ...send somewhere + * } + */ + uploadFile?: (ev: DragEvent) => void; + /** + * In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64. + * @default true + */ embedAsBase64?: boolean; - handleAdd?: Function; + /** + * Handle the image url submit from the built-in 'Add image' form. + * @example + * handleAdd: (textFromInput) => { + * // some check... + * editor.AssetManager.add(textFromInput); + * } + */ + handleAdd?: (value: string) => void; + /** + * Method called before upload, on return false upload is canceled. + * @example + * beforeUpload: (files) => { + * // logic... + * const stopUpload = true; + * if(stopUpload) return false; + * } + */ + beforeUpload?: (files: any) => void | false; + /** + * Toggles visiblity of assets url input + * @default true + */ + showUrlInput?: boolean; + /** + * Avoid rendering the default asset manager. + * @default false + */ + custom?: + | boolean + | { + open?: (props: any) => void; + close?: (props: any) => void; + }; + /** + * Enable an upload dropzone on the entire editor (not document) when dragging files over it. + * If active the dropzone disable/hide the upload dropzone in asset modal, otherwise you will get double drops (#507). + * @deprecated + */ dropzone?: boolean; - openAssetsOnDrop?: number; + /** + * Open the asset manager once files are been dropped via the dropzone. + * @deprecated + */ + openAssetsOnDrop?: boolean; + /** + * Any dropzone content to append inside dropzone element + * @deprecated + */ dropzoneContent?: string; - modalTitle?: string; - inputPlaceholder?: string; - custom?: boolean | { - open?: (props: any) => void, - close?: (props: any) => void, - }; } interface CanvasConfig { diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index d1c1f961f..b92fa4c12 100644 --- a/src/abstract/Module.ts +++ b/src/abstract/Module.ts @@ -1,4 +1,4 @@ -import { isElement, isUndefined } from 'underscore'; +import { isElement, isUndefined, isString } from 'underscore'; import { Collection, View } from '../common'; import EditorModel from '../editor/model/Editor'; import { createId, isDef, deepMerge } from '../utils/mixins'; @@ -108,8 +108,8 @@ export abstract class ItemManagerModule< protected all: TCollection; view?: View; - constructor(em: EditorModel, moduleName: string, all: any, events?: any) { - super(em, moduleName); + constructor(em: EditorModel, moduleName: string, all: any, events?: any, defaults?: TConf) { + super(em, moduleName, defaults); this.all = all; this.events = events; this.__initListen(); diff --git a/src/asset_manager/config/config.js b/src/asset_manager/config/config.js deleted file mode 100644 index cc6f6c4ec..000000000 --- a/src/asset_manager/config/config.js +++ /dev/null @@ -1,110 +0,0 @@ -export default { - // Default assets - // eg. [ - // 'https://...image1.png', - // 'https://...image2.png', - // {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1}, - // .. - // ] - assets: [], - - // Content to add where there is no assets to show - // eg. 'No assets here, drag to upload' - noAssets: '', - - // Style prefix - stylePrefix: 'am-', - - // Upload endpoint, set `false` to disable upload - // upload: 'https://endpoint/upload/assets', - // upload: false, - upload: 0, - - // The name used in POST to pass uploaded files - uploadName: 'files', - - // Custom headers to pass with the upload request - headers: {}, - - // Custom parameters to pass with the upload request, eg. csrf token - params: {}, - - // The credentials setting for the upload request, eg. 'include', 'omit' - credentials: 'include', - - // Allow uploading multiple files per request. - // If disabled filename will not have '[]' appended - multiUpload: true, - - // If true, tries to add automatically uploaded assets. - // To make it work the server should respond with a JSON containing assets - // in a data key, eg: - // { - // data: [ - // 'https://.../image.png', - // ... - // {src: 'https://.../image2.png'}, - // ... - // ] - // } - autoAdd: true, - - // To upload your assets, the module uses Fetch API, with this option you - // overwrite it with something else. - // It should return a Promise - // @example - // customFetch: (url, options) => axios(url, { data: options.body }), - customFetch: null, - - // Custom uploadFile function. - // Differently from the `customFetch` option, this gives a total control - // over the uploading process, but you also have to emit all `asset:upload:*` events - // by yourself (if you need to use them somewhere) - // @example - // uploadFile: (e) => { - // var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; - // // ...send somewhere - // } - uploadFile: null, - - // In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64 - embedAsBase64: true, - - // Handle the image url submit from the built-in 'Add image' form - // @example - // handleAdd: (textFromInput) => { - // // some check... - // editor.AssetManager.add(textFromInput); - // } - handleAdd: null, - - // Method called before upload, on return false upload is canceled. - // @example - // beforeUpload: (files) => { - // // logic... - // var stopUpload = true; - // if(stopUpload) return false; - // } - beforeUpload: null, - - // Toggles visiblity of assets url input - showUrlInput: true, - - // Avoid rendering the default asset manager. - custom: false, - - // WARNING: all the options below are considered DEPRECATED. - // --------------------------------------------------------------- - - // Enable an upload dropzone on the entire editor (not document) when dragging - // files over it - // If active the dropzone disable/hide the upload dropzone in asset modal, - // otherwise you will get double drops (#507) - dropzone: false, - - // Open the asset manager once files are been dropped via the dropzone - openAssetsOnDrop: 1, - - // Any dropzone content to append inside dropzone element - dropzoneContent: '', -}; diff --git a/src/asset_manager/config/config.ts b/src/asset_manager/config/config.ts new file mode 100644 index 000000000..a68910fdf --- /dev/null +++ b/src/asset_manager/config/config.ts @@ -0,0 +1,156 @@ +export interface AssetManagerConfig { + /** + * Default assets. + * @example + * [ + * 'https://...image1.png', + * 'https://...image2.png', + * {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1} + * ] + */ + assets?: (string | Record)[]; + /** + * Content to add where there is no assets to show. + * @default '' + * @example 'No assets here, drag to upload' + */ + noAssets?: string; + /** + * Style prefix + * @default 'am-' + */ + stylePrefix?: string; + /** + * Upload endpoint, set `false` to disable upload. + * @example 'https://endpoint/upload/assets' + */ + upload?: false | string; + /** + * The name used in POST to pass uploaded files. + * @default 'files' + */ + uploadName?: string; + /** + * Custom headers to pass with the upload request. + * @default {} + */ + headers?: Record; + /** + * Custom parameters to pass with the upload request, eg. csrf token. + * @default {} + */ + params?: Record; + /** + * The credentials setting for the upload request, eg. 'include', 'omit'. + * @default 'include' + */ + credentials?: RequestCredentials; + /** + * Allow uploading multiple files per request. If disabled filename will not have '[]' appended. + * @default true + */ + multiUpload?: boolean; + /** + * If true, tries to add automatically uploaded assets. To make it work the server should respond with a JSON containing assets in a data key, eg: + * { data: [ 'https://.../image.png', {src: 'https://.../image2.png'} ] + * @default true + */ + autoAdd?: boolean; + /** + * To upload your assets, the module uses Fetch API. With this option you can overwrite it with your own logic. The custom function should return a Promise. + * @example + * customFetch: (url, options) => axios(url, { data: options.body }), + */ + customFetch?: (url: string, options: Record) => Promise; + /** + * Custom uploadFile function. + * Differently from the `customFetch` option, this gives a total control over the uploading process, but you also have to emit all `asset:upload:*` events b + * y yourself (if you need to use them somewhere). + * @example + * uploadFile: (ev) => { + * const files = ev.dataTransfer ? ev.dataTransfer.files : ev.target.files; + * // ...send somewhere + * } + */ + uploadFile?: (ev: DragEvent) => void; + /** + * In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64. + * @default true + */ + embedAsBase64?: boolean; + /** + * Handle the image url submit from the built-in 'Add image' form. + * @example + * handleAdd: (textFromInput) => { + * // some check... + * editor.AssetManager.add(textFromInput); + * } + */ + handleAdd?: (value: string) => void; + /** + * Method called before upload, on return false upload is canceled. + * @example + * beforeUpload: (files) => { + * // logic... + * const stopUpload = true; + * if(stopUpload) return false; + * } + */ + beforeUpload?: (files: any) => void | false; + /** + * Toggles visiblity of assets url input + * @default true + */ + showUrlInput?: boolean; + /** + * Avoid rendering the default asset manager. + * @default false + */ + custom?: + | boolean + | { + open?: (props: any) => void; + close?: (props: any) => void; + }; + /** + * Enable an upload dropzone on the entire editor (not document) when dragging files over it. + * If active the dropzone disable/hide the upload dropzone in asset modal, otherwise you will get double drops (#507). + * @deprecated + */ + dropzone?: boolean; + /** + * Open the asset manager once files are been dropped via the dropzone. + * @deprecated + */ + openAssetsOnDrop?: boolean; + /** + * Any dropzone content to append inside dropzone element + * @deprecated + */ + dropzoneContent?: string; +} + +const config: AssetManagerConfig = { + assets: [], + noAssets: '', + stylePrefix: 'am-', + upload: '', + uploadName: 'files', + headers: {}, + params: {}, + credentials: 'include', + multiUpload: true, + autoAdd: true, + customFetch: undefined, + uploadFile: undefined, + embedAsBase64: true, + handleAdd: undefined, + beforeUpload: undefined, + showUrlInput: true, + custom: false, + dropzone: false, + openAssetsOnDrop: true, + dropzoneContent: '', +}; + +export default config; diff --git a/src/asset_manager/index.js b/src/asset_manager/index.js deleted file mode 100644 index 11d8eaab5..000000000 --- a/src/asset_manager/index.js +++ /dev/null @@ -1,472 +0,0 @@ -/** - * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.js) - * ```js - * const editor = grapesjs.init({ - * assetManager: { - * // options - * } - * }) - * ``` - * - * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance - * - * ```js - * const assetManager = editor.AssetManager; - * ``` - * - * ## Available Events - * * `asset:open` - Asset Manager opened. - * * `asset:close` - Asset Manager closed. - * * `asset:add` - Asset added. The [Asset] is passed as an argument to the callback. - * * `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback. - * * `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback. - * * `asset:upload:start` - Before the upload is started. - * * `asset:upload:end` - After the upload is ended. - * * `asset:upload:error` - On any error in upload, passes the error as an argument. - * * `asset:upload:response` - On upload response, passes the result as an argument. - * * `asset` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. - * * `asset:custom` - Event for handling custom Asset Manager UI. - * - * ## Methods - * * [open](#open) - * * [close](#close) - * * [isOpen](#isopen) - * * [add](#add) - * * [get](#get) - * * [getAll](#getall) - * * [getAllVisible](#getallvisible) - * * [remove](#remove) - * * [getContainer](#getcontainer) - * - * [Asset]: asset.html - * - * @module AssetManager - */ - -import { debounce, isFunction } from 'underscore'; -import { Module } from '../common'; -import defaults from './config/config'; -import Asset from './model/Assets'; -import Assets from './model/Assets'; -import AssetsView from './view/AssetsView'; -import FileUpload from './view/FileUploader'; - -export const evAll = 'asset'; -export const evPfx = `${evAll}:`; -export const evSelect = `${evPfx}select`; -export const evUpdate = `${evPfx}update`; -export const evAdd = `${evPfx}add`; -export const evRemove = `${evPfx}remove`; -export const evRemoveBefore = `${evRemove}:before`; -export const evCustom = `${evPfx}custom`; -export const evOpen = `${evPfx}open`; -export const evClose = `${evPfx}close`; -export const evUpload = `${evPfx}upload`; -export const evUploadStart = `${evUpload}:start`; -export const evUploadEnd = `${evUpload}:end`; -export const evUploadError = `${evUpload}:error`; -export const evUploadRes = `${evUpload}:response`; - -export default () => { - let c = {}; - let assets, assetsVis, am, fu; - const assetCmd = 'open-assets'; - - return { - ...Module, - - name: 'AssetManager', - - storageKey: 'assets', - - Asset, - - Assets, - - events: { - all: evAll, - select: evSelect, - update: evUpdate, - add: evAdd, - remove: evRemove, - removeBefore: evRemoveBefore, - custom: evCustom, - open: evOpen, - close: evClose, - uploadStart: evUploadStart, - uploadEnd: evUploadEnd, - uploadError: evUploadError, - uploadResponse: evUploadRes, - }, - - init(config = {}) { - c = { ...defaults, ...config }; - const ppfx = c.pStylePrefix; - const { em } = c; - this.config = c; - this.em = em; - - if (ppfx) { - c.stylePrefix = ppfx + c.stylePrefix; - } - - // Global assets collection - assets = new Assets([]); - assetsVis = new Assets([]); - this.all = assets; - this.__initListen(); - - // Setup the sync between the global and public collections - assets.on('add', model => this.getAllVisible().add(model)); - assets.on('remove', model => this.getAllVisible().remove(model)); - - return this; - }, - - __propEv(ev, ...data) { - this.em.trigger(ev, ...data); - this.getAll().trigger(ev, ...data); - }, - - __onAllEvent: debounce(function () { - this.__trgCustom(); - }), - - __trgCustom() { - const bhv = this.__getBehaviour(); - if (!bhv.container && !this.getConfig().custom.open) { - return; - } - this.em.trigger(this.events.custom, this.__customData()); - }, - - __customData() { - const bhv = this.__getBehaviour(); - return { - am: this, - open: this.isOpen(), - assets: this.getAll().models, - types: bhv.types || [], - container: bhv.container, - close: () => this.close(), - remove: (...args) => this.remove(...args), - select: (asset, complete) => { - const res = this.add(asset); - isFunction(bhv.select) && bhv.select(res, complete); - }, - // extra - options: bhv.options || {}, - }; - }, - - /** - * Open the asset manager. - * @param {Object} [options] Options for the asset manager. - * @param {Array} [options.types=['image']] Types of assets to show. - * @param {Function} [options.select] Type of operation to perform on asset selection. If not specified, nothing will happen. - * @example - * assetManager.open({ - * select(asset, complete) { - * const selected = editor.getSelected(); - * if (selected && selected.is('image')) { - * selected.addAttributes({ src: asset.getSrc() }); - * // The default AssetManager UI will trigger `select(asset, false)` on asset click - * // and `select(asset, true)` on double-click - * complete && assetManager.close(); - * } - * } - * }); - * // with your custom types (you should have assets with those types declared) - * assetManager.open({ types: ['doc'], ... }); - */ - open(options = {}) { - const cmd = this.em.get('Commands'); - cmd.run(assetCmd, { - types: ['image'], - select: () => {}, - ...options, - }); - }, - - /** - * Close the asset manager. - * @example - * assetManager.close(); - */ - close() { - const cmd = this.em.get('Commands'); - cmd.stop(assetCmd); - }, - - /** - * Checks if the asset manager is open - * @returns {Boolean} - * @example - * assetManager.isOpen(); // true | false - */ - isOpen() { - const cmd = this.em.get('Commands'); - return !!(cmd && cmd.isActive(assetCmd)); - }, - - /** - * Add new asset/s to the collection. URLs are supposed to be unique - * @param {String|Object|Array|Array} asset URL strings or an objects representing the resource. - * @param {Object} [opts] Options - * @returns {[Asset]} - * @example - * // As strings - * assetManager.add('http://img.jpg'); - * assetManager.add(['http://img.jpg', './path/to/img.png']); - * - * // Using objects you can indicate the type and other meta informations - * assetManager.add({ - * // type: 'image', // image is default - * src: 'http://img.jpg', - * height: 300, - * width: 200, - * }); - * assetManager.add([{ src: 'img2.jpg' }, { src: 'img2.png' }]); - */ - add(asset, opts = {}) { - // Put the model at the beginning - if (typeof opts.at == 'undefined') { - opts.at = 0; - } - - return assets.add(asset, opts); - }, - - /** - * Return asset by URL - * @param {String} src URL of the asset - * @returns {[Asset]|null} - * @example - * const asset = assetManager.get('http://img.jpg'); - */ - get(src) { - return assets.where({ src })[0] || null; - }, - - /** - * Return the global collection, containing all the assets - * @returns {Collection<[Asset]>} - */ - getAll() { - return assets; - }, - - /** - * Return the visible collection, which contains assets actually rendered - * @returns {Collection<[Asset]>} - */ - getAllVisible() { - return assetsVis; - }, - - /** - * Remove asset - * @param {String|[Asset]} asset Asset or asset URL - * @returns {[Asset]} Removed asset - * @example - * const removed = assetManager.remove('http://img.jpg'); - * // or by passing the Asset - * const asset = assetManager.get('http://img.jpg'); - * assetManager.remove(asset); - */ - remove(asset, opts) { - return this.__remove(asset, opts); - }, - - store() { - return this.getProjectData(); - }, - - load(data) { - return this.loadProjectData(data); - }, - - /** - * Return the Asset Manager Container - * @returns {HTMLElement} - */ - getContainer() { - const bhv = this.__getBehaviour(); - return bhv.container || (am && am.el); - }, - - /** - * Get assets element container - * @returns {HTMLElement} - * @private - */ - getAssetsEl() { - return am.el.querySelector('[data-el=assets]'); - }, - - /** - * Render assets - * @param {array} assets Assets to render, without the argument will render all global assets - * @returns {HTMLElement} - * @private - * @example - * // Render all assets - * assetManager.render(); - * - * // Render some of the assets - * const assets = assetManager.getAll(); - * assetManager.render(assets.filter( - * asset => asset.get('category') == 'cats' - * )); - */ - render(assts) { - if (this.getConfig().custom) return; - const toRender = assts || this.getAll().models; - - if (!am) { - const obj = this.__viewParams(); - obj.fu = this.FileUploader(); - const el = am && am.el; - am = new AssetsView({ - el, - ...obj, - }); - am.render(); - } - - assetsVis.reset(toRender); - return this.getContainer(); - }, - - __viewParams() { - return { - collection: assetsVis, // Collection visible in asset manager - globalCollection: assets, - config: c, - module: this, - }; - }, - - /** - * Add new type. If you want to get more about type definition we suggest to read the [module's page](/modules/Assets.html) - * @param {string} id Type ID - * @param {Object} definition Definition of the type. Each definition contains - * `model` (business logic), `view` (presentation logic) - * and `isType` function which recognize the type of the - * passed entity - * @private - * @example - * assetManager.addType('my-type', { - * model: {}, - * view: {}, - * isType: (value) => {}, - * }) - */ - addType(id, definition) { - this.getAll().addType(id, definition); - }, - - /** - * Get type - * @param {string} id Type ID - * @returns {Object} Type definition - * @private - */ - getType(id) { - return this.getAll().getType(id); - }, - - /** - * Get types - * @returns {Array} - * @private - */ - getTypes() { - return this.getAll().getTypes(); - }, - - //------- - - AssetsView() { - return am; - }, - - FileUploader() { - if (!fu) { - fu = new FileUpload(this.__viewParams()); - } - return fu; - }, - - onLoad() { - this.getAll().reset(c.assets); - const { em, events } = this; - em.on(`run:${assetCmd}`, () => this.__propEv(events.open)); - em.on(`stop:${assetCmd}`, () => this.__propEv(events.close)); - }, - - postRender(editorView) { - c.dropzone && fu && fu.initDropzone(editorView); - }, - - /** - * Set new target - * @param {Object} m Model - * @private - * */ - setTarget(m) { - assetsVis.target = m; - }, - - /** - * Set callback after asset was selected - * @param {Object} f Callback function - * @private - * */ - onSelect(f) { - assetsVis.onSelect = f; - }, - - /** - * Set callback to fire when the asset is clicked - * @param {function} func - * @private - */ - onClick(func) { - c.onClick = func; - }, - - /** - * Set callback to fire when the asset is double clicked - * @param {function} func - * @private - */ - onDblClick(func) { - c.onDblClick = func; - }, - - __behaviour(opts = {}) { - return (this._bhv = { - ...(this._bhv || {}), - ...opts, - }); - }, - - __getBehaviour(opts = {}) { - return this._bhv || {}; - }, - - destroy() { - assets.stopListening(); - assetsVis.stopListening(); - assets.reset(); - assetsVis.reset(); - fu && fu.remove(); - am && am.remove(); - [assets, assetsVis, am, fu].forEach(i => (i = null)); - this._bhv = {}; - this.all = {}; - c = {}; - }, - }; -}; diff --git a/src/asset_manager/index.ts b/src/asset_manager/index.ts new file mode 100644 index 000000000..6bb46293b --- /dev/null +++ b/src/asset_manager/index.ts @@ -0,0 +1,463 @@ +/** + * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.ts) + * ```js + * const editor = grapesjs.init({ + * assetManager: { + * // options + * } + * }) + * ``` + * + * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance + * + * ```js + * const assetManager = editor.AssetManager; + * ``` + * + * ## Available Events + * * `asset:open` - Asset Manager opened. + * * `asset:close` - Asset Manager closed. + * * `asset:add` - Asset added. The [Asset] is passed as an argument to the callback. + * * `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback. + * * `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback. + * * `asset:upload:start` - Before the upload is started. + * * `asset:upload:end` - After the upload is ended. + * * `asset:upload:error` - On any error in upload, passes the error as an argument. + * * `asset:upload:response` - On upload response, passes the result as an argument. + * * `asset` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. + * * `asset:custom` - Event for handling custom Asset Manager UI. + * + * ## Methods + * * [open](#open) + * * [close](#close) + * * [isOpen](#isopen) + * * [add](#add) + * * [get](#get) + * * [getAll](#getall) + * * [getAllVisible](#getallvisible) + * * [remove](#remove) + * * [getContainer](#getcontainer) + * + * [Asset]: asset.html + * + * @module Assets + */ + +import { debounce, isFunction } from 'underscore'; +import { ItemManagerModule } from '../abstract/Module'; +import EditorModel from '../editor/model/Editor'; +import defaults, { AssetManagerConfig } from './config/config'; +import Asset from './model/Asset'; +import Assets from './model/Assets'; +import AssetsView from './view/AssetsView'; +import FileUpload from './view/FileUploader'; + +export const evAll = 'asset'; +export const evPfx = `${evAll}:`; +export const evSelect = `${evPfx}select`; +export const evUpdate = `${evPfx}update`; +export const evAdd = `${evPfx}add`; +export const evRemove = `${evPfx}remove`; +export const evRemoveBefore = `${evRemove}:before`; +export const evCustom = `${evPfx}custom`; +export const evOpen = `${evPfx}open`; +export const evClose = `${evPfx}close`; +export const evUpload = `${evPfx}upload`; +export const evUploadStart = `${evUpload}:start`; +export const evUploadEnd = `${evUpload}:end`; +export const evUploadError = `${evUpload}:error`; +export const evUploadRes = `${evUpload}:response`; +const assetCmd = 'open-assets'; +const events = { + all: evAll, + select: evSelect, + update: evUpdate, + add: evAdd, + remove: evRemove, + removeBefore: evRemoveBefore, + custom: evCustom, + open: evOpen, + close: evClose, + uploadStart: evUploadStart, + uploadEnd: evUploadEnd, + uploadError: evUploadError, + uploadResponse: evUploadRes, +}; + +// TODO +type AssetProps = Record; + +export default class AssetManager extends ItemManagerModule { + storageKey = 'assets'; + Asset = Asset; + Assets = Assets; + assetsVis: Assets; + am?: AssetsView; + fu?: FileUpload; + _bhv?: any; + + /** + * Initialize module + * @param {Object} config Configurations + * @private + */ + constructor(em: EditorModel) { + // @ts-ignore + super(em, 'AssetManager', new Assets([], em), events, defaults); + const { all, config } = this; + // @ts-ignore + this.assetsVis = new Assets([]); + // @ts-ignore + const ppfx = config.pStylePrefix; + if (ppfx) { + config.stylePrefix = `${ppfx}${config.stylePrefix}`; + } + + // Setup the sync between the global and public collections + all.on('add', (model: Asset) => this.getAllVisible().add(model)); + all.on('remove', (model: Asset) => this.getAllVisible().remove(model)); + + this.__onAllEvent = debounce(() => this.__trgCustom(), 0); + + return this; + } + + __propEv(ev: string, ...data: any[]) { + this.em.trigger(ev, ...data); + this.getAll().trigger(ev, ...data); + } + + __trgCustom() { + const bhv = this.__getBehaviour(); + if (!bhv.container && !this.getConfig().custom.open) { + return; + } + this.em.trigger(this.events.custom, this.__customData()); + } + + __customData() { + const bhv = this.__getBehaviour(); + return { + am: this, + open: this.isOpen(), + assets: this.getAll().models, + types: bhv.types || [], + container: bhv.container, + close: () => this.close(), + remove: (asset: string | Asset, opts?: Record) => this.remove(asset, opts), + select: (asset: Asset, complete: boolean) => { + const res = this.add(asset); + isFunction(bhv.select) && bhv.select(res, complete); + }, + // extra + options: bhv.options || {}, + }; + } + + /** + * Open the asset manager. + * @param {Object} [options] Options for the asset manager. + * @param {Array} [options.types=['image']] Types of assets to show. + * @param {Function} [options.select] Type of operation to perform on asset selection. If not specified, nothing will happen. + * @example + * assetManager.open({ + * select(asset, complete) { + * const selected = editor.getSelected(); + * if (selected && selected.is('image')) { + * selected.addAttributes({ src: asset.getSrc() }); + * // The default AssetManager UI will trigger `select(asset, false)` on asset click + * // and `select(asset, true)` on double-click + * complete && assetManager.close(); + * } + * } + * }); + * // with your custom types (you should have assets with those types declared) + * assetManager.open({ types: ['doc'], ... }); + */ + open(options = {}) { + const cmd = this.em.get('Commands'); + cmd.run(assetCmd, { + types: ['image'], + select: () => {}, + ...options, + }); + } + + /** + * Close the asset manager. + * @example + * assetManager.close(); + */ + close() { + const cmd = this.em.get('Commands'); + cmd.stop(assetCmd); + } + + /** + * Checks if the asset manager is open + * @returns {Boolean} + * @example + * assetManager.isOpen(); // true | false + */ + isOpen() { + const cmd = this.em.get('Commands'); + return !!cmd?.isActive(assetCmd); + } + + /** + * Add new asset/s to the collection. URLs are supposed to be unique + * @param {String|Object|Array|Array} asset URL strings or an objects representing the resource. + * @param {Object} [opts] Options + * @returns {[Asset]} + * @example + * // As strings + * assetManager.add('http://img.jpg'); + * assetManager.add(['http://img.jpg', './path/to/img.png']); + * + * // Using objects you can indicate the type and other meta informations + * assetManager.add({ + * // type: 'image', // image is default + * src: 'http://img.jpg', + * height: 300, + * width: 200, + * }); + * assetManager.add([{ src: 'img2.jpg' }, { src: 'img2.png' }]); + */ + add(asset: string | AssetProps | (string | AssetProps)[], opts: Record = {}) { + // Put the model at the beginning + if (typeof opts.at == 'undefined') { + opts.at = 0; + } + + return this.all.add(asset, opts); + } + + /** + * Return asset by URL + * @param {String} src URL of the asset + * @returns {[Asset]|null} + * @example + * const asset = assetManager.get('http://img.jpg'); + */ + get(src: string): Asset | null { + return this.all.where({ src })[0] || null; + } + + /** + * Return the global collection, containing all the assets + * @returns {Collection<[Asset]>} + */ + // @ts-ignore + getAll() { + return this.all; + } + + /** + * Return the visible collection, which contains assets actually rendered + * @returns {Collection<[Asset]>} + */ + getAllVisible() { + return this.assetsVis; + } + + /** + * Remove asset + * @param {String|[Asset]} asset Asset or asset URL + * @returns {[Asset]} Removed asset + * @example + * const removed = assetManager.remove('http://img.jpg'); + * // or by passing the Asset + * const asset = assetManager.get('http://img.jpg'); + * assetManager.remove(asset); + */ + remove(asset: string | Asset, opts?: Record) { + return this.__remove(asset, opts); + } + + store() { + return this.getProjectData(); + } + + load(data: Record) { + return this.loadProjectData(data); + } + + /** + * Return the Asset Manager Container + * @returns {HTMLElement} + */ + getContainer() { + const bhv = this.__getBehaviour(); + return bhv.container || this.am?.el; + } + + /** + * Get assets element container + * @returns {HTMLElement} + * @private + */ + getAssetsEl() { + return this.am?.el.querySelector('[data-el=assets]'); + } + + /** + * Render assets + * @param {array} assets Assets to render, without the argument will render all global assets + * @returns {HTMLElement} + * @private + * @example + * // Render all assets + * assetManager.render(); + * + * // Render some of the assets + * const assets = assetManager.getAll(); + * assetManager.render(assets.filter( + * asset => asset.get('category') == 'cats' + * )); + */ + render(assts?: Asset[]) { + if (this.getConfig().custom) return; + const toRender = assts || this.getAll().models; + + if (!this.am) { + const obj = this.__viewParams(); + obj.fu = this.FileUploader(); + this.am = new AssetsView({ ...obj }); + this.am.render(); + } + + this.assetsVis.reset(toRender); + return this.getContainer(); + } + + __viewParams() { + return { + collection: this.assetsVis, // Collection visible in asset manager + globalCollection: this.all, + config: this.config, + module: this, + fu: undefined as any, + }; + } + + /** + * Add new type. If you want to get more about type definition we suggest to read the [module's page](/modules/Assets.html) + * @param {string} id Type ID + * @param {Object} definition Definition of the type. Each definition contains + * `model` (business logic), `view` (presentation logic) + * and `isType` function which recognize the type of the + * passed entity + * @private + * @example + * assetManager.addType('my-type', { + * model: {}, + * view: {}, + * isType: (value) => {}, + * }) + */ + addType(id: string, definition: any) { + this.getAll().addType(id, definition); + } + + /** + * Get type + * @param {string} id Type ID + * @returns {Object} Type definition + * @private + */ + getType(id: string) { + return this.getAll().getType(id); + } + + /** + * Get types + * @returns {Array} + * @private + */ + getTypes() { + return this.getAll().getTypes(); + } + + //------- + + AssetsView() { + return this.am; + } + + FileUploader() { + if (!this.fu) { + this.fu = new FileUpload(this.__viewParams()); + } + return this.fu; + } + + onLoad() { + this.getAll().reset(this.config.assets); + const { em, events } = this; + em.on(`run:${assetCmd}`, () => this.__propEv(events.open)); + em.on(`stop:${assetCmd}`, () => this.__propEv(events.close)); + } + + postRender(editorView: any) { + this.config.dropzone && this.fu?.initDropzone(editorView); + } + + /** + * Set new target + * @param {Object} m Model + * @private + * */ + setTarget(m: any) { + this.assetsVis.target = m; + } + + /** + * Set callback after asset was selected + * @param {Object} f Callback function + * @private + * */ + onSelect(f: any) { + this.assetsVis.onSelect = f; + } + + /** + * Set callback to fire when the asset is clicked + * @param {function} func + * @private + */ + onClick(func: any) { + // @ts-ignore + this.config.onClick = func; + } + + /** + * Set callback to fire when the asset is double clicked + * @param {function} func + * @private + */ + onDblClick(func: any) { + // @ts-ignore + this.config.onDblClick = func; + } + + __behaviour(opts = {}) { + return (this._bhv = { + ...(this._bhv || {}), + ...opts, + }); + } + + __getBehaviour(opts = {}) { + return this._bhv || {}; + } + + destroy() { + this.all.stopListening(); + this.all.reset(); + this.assetsVis.stopListening(); + this.assetsVis.reset(); + this.fu?.remove(); + this.am?.remove(); + this._bhv = {}; + } +} diff --git a/src/asset_manager/model/Asset.js b/src/asset_manager/model/Asset.ts similarity index 94% rename from src/asset_manager/model/Asset.js rename to src/asset_manager/model/Asset.ts index 25ee99c36..91d80a997 100644 --- a/src/asset_manager/model/Asset.js +++ b/src/asset_manager/model/Asset.ts @@ -8,6 +8,10 @@ import { Model } from '../../common'; * @module docsjs.Asset */ export default class Asset extends Model { + static getDefaults() { + return result(this.prototype, 'defaults'); + } + defaults() { return { type: '', @@ -65,7 +69,3 @@ export default class Asset extends Model { } Asset.prototype.idAttribute = 'src'; - -Asset.getDefaults = function () { - return result(this.prototype, 'defaults'); -}; diff --git a/src/asset_manager/model/AssetImage.js b/src/asset_manager/model/AssetImage.ts similarity index 100% rename from src/asset_manager/model/AssetImage.js rename to src/asset_manager/model/AssetImage.ts diff --git a/src/asset_manager/model/Assets.js b/src/asset_manager/model/Assets.ts similarity index 70% rename from src/asset_manager/model/Assets.js rename to src/asset_manager/model/Assets.ts index 9ed3704a2..69dd0b4af 100644 --- a/src/asset_manager/model/Assets.js +++ b/src/asset_manager/model/Assets.ts @@ -1,16 +1,19 @@ import { Collection } from '../../common'; +import Asset from './Asset'; import AssetImage from './AssetImage'; import AssetImageView from './../view/AssetImageView'; import TypeableCollection from '../../domain_abstract/model/TypeableCollection'; -export default class Assets extends Collection.extend(TypeableCollection) {} +const TypeableCollectionExt = Collection.extend(TypeableCollection); + +export default class Assets extends TypeableCollectionExt {} Assets.prototype.types = [ { id: 'image', model: AssetImage, view: AssetImageView, - isType(value) { + isType(value: string) { if (typeof value == 'string') { return { type: 'image', diff --git a/src/asset_manager/view/AssetImageView.js b/src/asset_manager/view/AssetImageView.ts similarity index 89% rename from src/asset_manager/view/AssetImageView.js rename to src/asset_manager/view/AssetImageView.ts index 30db0e60a..e384590c8 100644 --- a/src/asset_manager/view/AssetImageView.js +++ b/src/asset_manager/view/AssetImageView.ts @@ -1,8 +1,9 @@ import { isFunction } from 'underscore'; import AssetView from './AssetView'; +import AssetImage from '../model/AssetImage'; import html from '../../utils/html'; -export default class AssetImageView extends AssetView { +export default class AssetImageView extends AssetView { getPreview() { const { pfx, ppfx, model } = this; const src = model.get('src'); @@ -26,6 +27,7 @@ export default class AssetImageView extends AssetView { `; } + // @ts-ignore init(o) { const pfx = this.pfx; this.className += ` ${pfx}asset-image`; @@ -38,6 +40,7 @@ export default class AssetImageView extends AssetView { onClick() { const { model, pfx } = this; const { select } = this.__getBhv(); + // @ts-ignore const { onClick } = this.config; const coll = this.collection; coll.trigger('deselectAll'); @@ -48,6 +51,7 @@ export default class AssetImageView extends AssetView { } else if (isFunction(onClick)) { onClick(model); } else { + // @ts-ignore this.updateTarget(coll.target); } } @@ -59,7 +63,9 @@ export default class AssetImageView extends AssetView { onDblClick() { const { em, model } = this; const { select } = this.__getBhv(); + // @ts-ignore const { onDblClick } = this.config; + // @ts-ignore const { target, onSelect } = this.collection; if (isFunction(select)) { @@ -77,13 +83,14 @@ export default class AssetImageView extends AssetView { * Remove asset from collection * @private * */ - onRemove(e) { + onRemove(e: Event) { e.stopImmediatePropagation(); this.model.collection.remove(this.model); } } AssetImageView.prototype.events = { + // @ts-ignore 'click [data-toggle=asset-remove]': 'onRemove', click: 'onClick', dblclick: 'onDblClick', diff --git a/src/asset_manager/view/AssetView.js b/src/asset_manager/view/AssetView.ts similarity index 57% rename from src/asset_manager/view/AssetView.js rename to src/asset_manager/view/AssetView.ts index 1fecf2015..e37fe2b45 100644 --- a/src/asset_manager/view/AssetView.js +++ b/src/asset_manager/view/AssetView.ts @@ -1,20 +1,40 @@ import { View } from '../../common'; +import Asset from '../model/Asset'; +import Assets from '../model/Assets'; +import { AssetManagerConfig } from '../config/config'; import { clone } from 'underscore'; +import EditorModel from '../../editor/model/Editor'; -export default class AssetView extends View { - initialize(o = {}) { - this.options = o; - this.collection = o.collection; - const config = o.config || {}; +export type AssetViewProps = Backbone.ViewOptions & { + collection: Assets; + config: AssetManagerConfig; +}; + +export default class AssetView extends View { + pfx: string; + ppfx: string; + options: AssetViewProps; + config: AssetManagerConfig; + em: EditorModel; + init?: (opt: AssetViewProps) => void; + + constructor(opt: AssetViewProps) { + super(opt as any); + this.options = opt; + this.collection = opt.collection; + const config = opt.config || {}; this.config = config; this.pfx = config.stylePrefix || ''; + // @ts-ignore this.ppfx = config.pStylePrefix || ''; + // @ts-ignore this.em = config.em; this.className = this.pfx + 'asset'; this.listenTo(this.model, 'destroy remove', this.remove); + // @ts-ignore this.model.view = this; const init = this.init && this.init.bind(this); - init && init(o); + init && init(opt); } __getBhv() { @@ -23,8 +43,8 @@ export default class AssetView extends View { return (am && am.__getBehaviour()) || {}; } - template() { - const pfx = this.pfx; + template(view: AssetView, asset: Asset) { + const { pfx } = this; return `
${this.getPreview()} @@ -43,7 +63,7 @@ export default class AssetView extends View { * @param {Model} target * @private * */ - updateTarget(target) { + updateTarget(target: any) { if (target && target.set) { target.set('attributes', clone(target.get('attributes'))); target.set('src', this.model.get('src')); @@ -61,7 +81,7 @@ export default class AssetView extends View { render() { const el = this.el; el.innerHTML = this.template(this, this.model); - el.className = this.className; + el.className = this.className!; return this; } } diff --git a/src/asset_manager/view/AssetsView.js b/src/asset_manager/view/AssetsView.ts similarity index 76% rename from src/asset_manager/view/AssetsView.js rename to src/asset_manager/view/AssetsView.ts index 5665f4144..69b9f9c2b 100644 --- a/src/asset_manager/view/AssetsView.js +++ b/src/asset_manager/view/AssetsView.ts @@ -1,15 +1,25 @@ import { View } from '../../common'; +import EditorModel from '../../editor/model/Editor'; +import { AssetManagerConfig } from '../config/config'; +import Asset from '../model/Asset'; export default class AssetsView extends View { - template({ pfx, ppfx, em }) { + options: any; + config: AssetManagerConfig; + pfx: string; + ppfx: string; + em: EditorModel; + inputUrl?: HTMLInputElement | null; + + template({ pfx, ppfx, em }: AssetsView) { let form = ''; if (this.config.showUrlInput) { form = `
- +
- +
`; @@ -26,11 +36,14 @@ export default class AssetsView extends View { `; } - initialize(o) { + constructor(o: any = {}) { + super(o); this.options = o; this.config = o.config; this.pfx = this.config.stylePrefix || ''; + // @ts-ignore this.ppfx = this.config.pStylePrefix || ''; + // @ts-ignore this.em = this.config.em; const coll = this.collection; this.listenTo(coll, 'reset', this.renderAssets); @@ -45,8 +58,8 @@ export default class AssetsView extends View { * @return {this} * @private */ - handleSubmit(e) { - e.preventDefault(); + handleSubmit(ev: Event) { + ev.preventDefault(); const input = this.getAddInput(); const url = input && input.value.trim(); const handleAdd = this.config.handleAdd; @@ -56,7 +69,11 @@ export default class AssetsView extends View { } input.value = ''; - this.getAssetsEl().scrollTop = 0; + const assetsEl = this.getAssetsEl(); + + if (assetsEl) { + assetsEl.scrollTop = 0; + } if (handleAdd) { handleAdd.bind(this)(url); @@ -81,7 +98,9 @@ export default class AssetsView extends View { * @private */ getAddInput() { - if (!this.inputUrl || !this.inputUrl.value) this.inputUrl = this.el.querySelector(`.${this.pfx}add-asset input`); + if (!this.inputUrl || !this.inputUrl.value) { + this.inputUrl = this.el.querySelector(`.${this.pfx}add-asset input`); + } return this.inputUrl; } @@ -90,7 +109,7 @@ export default class AssetsView extends View { * @param {Asset} model Removed asset * @private */ - removedAsset(model) { + removedAsset(model: Asset) { if (!this.collection.length) { this.toggleNoAssets(); } @@ -100,9 +119,9 @@ export default class AssetsView extends View { * Add asset to collection * @private * */ - addToAsset(model) { + addToAsset(model: Asset) { if (this.collection.length == 1) { - this.toggleNoAssets(1); + this.toggleNoAssets(true); } this.addAsset(model); } @@ -114,10 +133,11 @@ export default class AssetsView extends View { * @return Object Object created * @private * */ - addAsset(model, fragmentEl = null) { + addAsset(model: Asset, fragmentEl: DocumentFragment | null = null) { const fragment = fragmentEl; const collection = this.collection; const config = this.config; + // @ts-ignore const rendered = new model.typeView({ model, collection, @@ -141,7 +161,7 @@ export default class AssetsView extends View { * @param {Boolean} hide * @private */ - toggleNoAssets(hide) { + toggleNoAssets(hide: boolean = false) { const assetsEl = this.$el.find(`.${this.pfx}assets`); if (hide) { @@ -165,7 +185,7 @@ export default class AssetsView extends View { const fragment = document.createDocumentFragment(); const assets = this.$el.find(`.${this.pfx}assets`); assets.empty(); - this.toggleNoAssets(this.collection.length); + this.toggleNoAssets(!!this.collection.length); this.collection.each(model => this.addAsset(model, fragment)); assets.append(fragment); } @@ -181,5 +201,6 @@ export default class AssetsView extends View { } AssetsView.prototype.events = { + // @ts-ignore submit: 'handleSubmit', }; diff --git a/src/asset_manager/view/FileUploader.js b/src/asset_manager/view/FileUploader.ts similarity index 53% rename from src/asset_manager/view/FileUploader.js rename to src/asset_manager/view/FileUploader.ts index d298a82aa..9ef69c8e5 100644 --- a/src/asset_manager/view/FileUploader.js +++ b/src/asset_manager/view/FileUploader.ts @@ -1,13 +1,36 @@ import { View } from '../../common'; +import EditorModel from '../../editor/model/Editor'; import fetch from '../../utils/fetch'; import html from '../../utils/html'; +import { AssetManagerConfig } from '../config/config'; + +type FileUploaderTemplateProps = { + pfx: string; + title: string; + uploadId: string; + disabled: boolean; + multiUpload: boolean; +}; export default class FileUploaderView extends View { - template({ pfx, title, uploadId, disabled, multiUpload }) { + options: any; + config: AssetManagerConfig; + pfx: string; + ppfx: string; + em: EditorModel; + module: any; + target: any; + uploadId: string; + disabled: boolean; + multiUpload: boolean; + uploadForm?: HTMLFormElement | null; + + template({ pfx, title, uploadId, disabled, multiUpload }: FileUploaderTemplateProps) { return html`
${title}
void) { const { module, config, target } = this; let json; try { @@ -100,7 +128,7 @@ export default class FileUploaderView extends View { } this.onUploadEnd(text); - clb && clb(json); + clb?.(json); } /** @@ -109,7 +137,8 @@ export default class FileUploaderView extends View { * @return {Promise} * @private * */ - uploadFile(e, clb) { + uploadFile(e: DragEvent, clb?: () => void) { + // @ts-ignore const files = e.dataTransfer ? e.dataTransfer.files : e.target.files; const { config } = this; const { beforeUpload } = config; @@ -129,12 +158,12 @@ export default class FileUploaderView extends View { body.append(`${config.uploadName}[]`, files[i]); } } else if (files.length) { - body.append(config.uploadName, files[0]); + body.append(config.uploadName!, files[0]); } var target = this.target; const url = config.upload; - const headers = config.headers; + const headers = config.headers!; const reqHead = 'X-Requested-With'; if (typeof headers[reqHead] == 'undefined') { @@ -151,10 +180,12 @@ export default class FileUploaderView extends View { }; const fetchResult = customFetch ? customFetch(url, fetchOpts) - : fetch(url, fetchOpts).then(res => - ((res.status / 200) | 0) == 1 ? res.text() : res.text().then(text => Promise.reject(text)) + : fetch(url, fetchOpts).then((res: any) => + ((res.status / 200) | 0) == 1 ? res.text() : res.text().then((text: string) => Promise.reject(text)) ); - return fetchResult.then(text => this.onUploadResponse(text, clb)).catch(err => this.onUploadError(err)); + return fetchResult + .then((text: string) => this.onUploadResponse(text, clb)) + .catch((err: Error) => this.onUploadError(err)); } } @@ -164,29 +195,31 @@ export default class FileUploaderView extends View { * */ initDrop() { var that = this; + if (!this.uploadForm) { - this.uploadForm = this.$el.find('form').get(0); - if ('draggable' in this.uploadForm) { - var uploadFile = this.uploadFile; + this.uploadForm = this.$el.find('form').get(0)!; + const formEl = this.uploadForm; + + if ('draggable' in formEl) { this.uploadForm.ondragover = function () { - this.className = that.pfx + 'hover'; + formEl.className = that.pfx + 'hover'; return false; }; this.uploadForm.ondragleave = function () { - this.className = ''; + formEl.className = ''; return false; }; - this.uploadForm.ondrop = function (e) { - this.className = ''; - e.preventDefault(); - that.uploadFile(e); + this.uploadForm.ondrop = function (ev) { + formEl.className = ''; + ev.preventDefault(); + that.uploadFile(ev); return; }; } } } - initDropzone(ev) { + initDropzone(ev: any) { let addedCls = 0; const c = this.config; const em = ev.model; @@ -212,7 +245,7 @@ export default class FileUploaderView extends View { cleanEditorElCls(); return false; }; - const onDrop = e => { + const onDrop = (e: DragEvent) => { cleanEditorElCls(); e.preventDefault(); e.stopPropagation(); @@ -259,111 +292,116 @@ export default class FileUploaderView extends View { $el.attr('class', pfx + 'file-uploader'); return this; } -} -FileUploaderView.embedAsBase64 = function (e, clb) { - // List files dropped - const files = e.dataTransfer ? e.dataTransfer.files : e.target.files; - const response = { data: [] }; + static embedAsBase64(e: DragEvent, clb?: () => void) { + // List files dropped + // @ts-ignore + const files = e.dataTransfer ? e.dataTransfer.files : e.target.files; + const response: Record = { data: [] }; - // Unlikely, widely supported now - if (!FileReader) { - this.onUploadError(new Error('Unsupported platform, FileReader is not defined')); - return; - } + // Unlikely, widely supported now + if (!FileReader) { + // @ts-ignore + this.onUploadError(new Error('Unsupported platform, FileReader is not defined')); + return; + } - const promises = []; - const mimeTypeMatcher = /^(.+)\/(.+)$/; - - for (const file of files) { - // For each file a reader (to read the base64 URL) - // and a promise (to track and merge results and errors) - const promise = new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener('load', event => { - let type; - const name = file.name; - - // Try to find the MIME type of the file. - const match = mimeTypeMatcher.exec(file.type); - if (match) { - type = match[1]; // The first part in the MIME, "image" in image/png - } else { - type = file.type; + const promises = []; + const mimeTypeMatcher = /^(.+)\/(.+)$/; + + for (const file of files) { + // For each file a reader (to read the base64 URL) + // and a promise (to track and merge results and errors) + const promise = new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener('load', event => { + let type; + const name = file.name; + + // Try to find the MIME type of the file. + const match = mimeTypeMatcher.exec(file.type); + if (match) { + type = match[1]; // The first part in the MIME, "image" in image/png + } else { + type = file.type; + } + + /* + // Show local video files, http://jsfiddle.net/dsbonev/cCCZ2/embedded/result,js,html,css/ + var URL = window.URL || window.webkitURL + var file = this.files[0] + var type = file.type + var videoNode = document.createElement('video'); + var canPlay = videoNode.canPlayType(type) // can use also for 'audio' types + if (canPlay === '') canPlay = 'no' + var message = 'Can play type "' + type + '": ' + canPlay + var isError = canPlay === 'no' + displayMessage(message, isError) + + if (isError) { + return } - /* - // Show local video files, http://jsfiddle.net/dsbonev/cCCZ2/embedded/result,js,html,css/ - var URL = window.URL || window.webkitURL - var file = this.files[0] - var type = file.type - var videoNode = document.createElement('video'); - var canPlay = videoNode.canPlayType(type) // can use also for 'audio' types - if (canPlay === '') canPlay = 'no' - var message = 'Can play type "' + type + '": ' + canPlay - var isError = canPlay === 'no' - displayMessage(message, isError) - - if (isError) { - return - } + var fileURL = URL.createObjectURL(file) + videoNode.src = fileURL + */ + + // If it's an image, try to find its size + if (type === 'image') { + const data = { + src: reader.result, + name, + type, + height: 0, + width: 0, + }; + + const image = new Image(); + image.addEventListener('error', error => { + reject(error); + }); + image.addEventListener('load', () => { + data.height = image.height; + data.width = image.width; + resolve(data); + }); + // @ts-ignore + image.src = data.src; + } else if (type) { + // Not an image, but has a type + resolve({ + src: reader.result, + name, + type, + }); + } else { + // No type found, resolve with the URL only + resolve(reader.result); + } + }); + reader.addEventListener('error', error => { + reject(error); + }); + reader.addEventListener('abort', error => { + reject('Aborted'); + }); - var fileURL = URL.createObjectURL(file) - videoNode.src = fileURL - */ - - // If it's an image, try to find its size - if (type === 'image') { - const data = { - src: reader.result, - name, - type, - height: 0, - width: 0, - }; - - const image = new Image(); - image.addEventListener('error', error => { - reject(error); - }); - image.addEventListener('load', () => { - data.height = image.height; - data.width = image.width; - resolve(data); - }); - image.src = data.src; - } else if (type) { - // Not an image, but has a type - resolve({ - src: reader.result, - name, - type, - }); - } else { - // No type found, resolve with the URL only - resolve(reader.result); - } - }); - reader.addEventListener('error', error => { - reject(error); - }); - reader.addEventListener('abort', error => { - reject('Aborted'); + reader.readAsDataURL(file); }); - reader.readAsDataURL(file); - }); + promises.push(promise); + } - promises.push(promise); + Promise.all(promises).then( + data => { + response.data = data; + // @ts-ignore + this.onUploadResponse(response, clb); + }, + error => { + // @ts-ignore + this.onUploadError(error); + } + ); } - - Promise.all(promises).then( - data => { - response.data = data; - this.onUploadResponse(response, clb); - }, - error => { - this.onUploadError(error); - } - ); -}; +} diff --git a/test/specs/asset_manager/index.js b/test/specs/asset_manager/index.js index 94b598da4..d240bce9c 100644 --- a/test/specs/asset_manager/index.js +++ b/test/specs/asset_manager/index.js @@ -1,4 +1,5 @@ import AssetManager from 'asset_manager'; +import Editor from 'editor'; describe('Asset Manager', () => { describe('Main', () => { @@ -13,7 +14,7 @@ describe('Asset Manager', () => { width: 101, height: 102, }; - obj = new AssetManager(); + obj = new AssetManager(new Editor()); obj.init(); document.body.querySelector('#asset-c').appendChild(obj.render()); });