diff --git a/.gitignore b/.gitignore index 641d5eb..4b2b4e5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ npm-debug.log .idea/ .DS_Store dist +package-lock.json diff --git a/package.json b/package.json index 0962db6..6f8f097 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "email": "team@codex.so" }, "devDependencies": { + "@editorjs/editorjs": "^2.30.5", + "typescript": "^5.5.4", "vite": "^4.5.0", "vite-plugin-css-injected-by-js": "^3.3.0" }, "dependencies": { - "@codexteam/icons": "^0.0.6" + "@codexteam/icons": "^0.3.2" } } diff --git a/src/index.js b/src/index.ts similarity index 56% rename from src/index.js rename to src/index.ts index e53ffa7..61940ba 100644 --- a/src/index.js +++ b/src/index.ts @@ -5,33 +5,200 @@ import './index.css'; import { IconAddBorder, IconStretch, IconAddBackground } from '@codexteam/icons'; +import type { API, FilePasteEvent, HTMLPasteEvent, PasteEvent, PatternPasteEvent } from "@editorjs/editorjs" + /** * SimpleImage Tool for the Editor.js * Works only with pasted image URLs and requires no server-side uploader. - * - * @typedef {object} SimpleImageData - * @description Tool's input and output data format - * @property {string} url — image URL - * @property {string} caption — image caption - * @property {boolean} withBorder - should image be rendered with border - * @property {boolean} withBackground - should image be rendered with background - * @property {boolean} stretched - should image be stretched to full width of container */ + +/** + * Tool's input and output data format + */ +export interface SimpleImageData { + /** + * image URL + */ + url: string; + /** + * image caption + */ + caption: string; + /** + * should image be rendered with border + */ + withBorder?: boolean; + /** + * should image be rendered with background + */ + withBackground?: boolean; + /** + * should image be stretched to full width of container + */ + stretched?: boolean; +} + +/** + * Represents the parameters used on the constructor inside the SimpleImage class + */ +interface SimpleImageParams { + /** + * previously saved data + */ + data: SimpleImageData; + /** + * user config for Tool + */ + config:object; + /** + * Editor.js API + */ + api: API; + /** + * read-only mode flag + */ + readOnly: boolean; +} + +/** + * Represents the styles and tools of a image + */ +interface SimpleImageCSS { + /** + * The base CSS class for the component, defining general styling for the entire element. + */ + baseClass:string, + /** + * CSS class applied when the image or component is in a loading state. + */ + loading: string, + /** + * CSS class applied to the input element. + */ + input:string, + /** + * Tool's classes + */ + wrapper: string, + /** + * Controlling the layout and appearance of the area where the image is displayed. + */ + imageHolder: string, + /** + * Defining styles for the text or label associated with the image. + */ + caption: string, +} + +/** + * Represents a single tune option for the tool + */ +interface Tune { + /** + * The name of the tune + */ + name: string; + /** + * The label displayed + */ + label: string; + /** + * The icon representing the tune, can be any type + */ + icon: any; +} + +/** + * Represents the nodes (HTML elements) used in the tool + */ +interface Nodes { + /** + * The main wrapper element for the tool + */ + wrapper: HTMLElement | null; + /** + * The container element for holding the image + */ + imageHolder: HTMLElement | null; + /** + * The image element + */ + image: HTMLImageElement | null; + /** + * The element used for displaying the image caption + */ + caption: HTMLElement | null; +} + +/** + * Returns image tunes config + */ +interface TuneSetting { + /** + * Name of the tune setting + */ + name: string; + /** + * Label of the tune setting + */ + label: string; + /** + * Toogle tune + */ + toggle: boolean; + /** + * Function that will run on activate + */ + onActivate: () => void; + /** + * Property that will set if tune setting is active + */ + isActive: boolean; +} + export default class SimpleImage { /** * Render plugin`s main Element and fill it with saved data - * - * @param {{data: SimpleImageData, config: object, api: object}} - * data — previously saved data - * config - user config for Tool - * api - Editor.js API - * readOnly - read-only mode flag */ - constructor({ data, config, api, readOnly }) { + + /** + * Editor.js API instance + */ + private api: API; + /** + * Flag indicating read-only mode + */ + private readOnly: boolean; + /** + * The index of the current block in the editor + */ + private blockIndex: number; + /** + * Stores current block data internally + */ + private _data: SimpleImageData; + /** + * CSS classes used for styling the tool + */ + private CSS: SimpleImageCSS; + /** + * Nodes cache + */ + private nodes: Nodes; + /** + * Represents an array of tunes + */ + private tunes: Tune[]; + + + constructor({ data, config, api, readOnly }: SimpleImageParams) { /** - * Editor.js API + * Editor.js API */ this.api = api; + /** + * Read-only mode + */ this.readOnly = readOnly; /** @@ -40,7 +207,6 @@ export default class SimpleImage { * So real block index will be +1 after rendering * * @todo place it at the `rendered` event hook to get real block index without +1; - * @type {number} */ this.blockIndex = this.api.blocks.getCurrentBlockIndex() + 1; @@ -51,7 +217,6 @@ export default class SimpleImage { baseClass: this.api.styles.block, loading: this.api.styles.loader, input: this.api.styles.input, - /** * Tool's classes */ @@ -112,14 +277,17 @@ export default class SimpleImage { * @public */ render() { - const wrapper = this._make('div', [this.CSS.baseClass, this.CSS.wrapper]), - loader = this._make('div', this.CSS.loading), - imageHolder = this._make('div', this.CSS.imageHolder), - image = this._make('img'), + /** + * Specific return as on each of the _make + */ + const wrapper = this._make('div',[this.CSS.baseClass, this.CSS.wrapper]) as HTMLElement, + loader = this._make('div', this.CSS.loading) as HTMLElement, + imageHolder = this._make('div', this.CSS.imageHolder) as HTMLElement, + image = this._make('img') as HTMLImageElement, caption = this._make('div', [this.CSS.input, this.CSS.caption], { contentEditable: !this.readOnly, innerHTML: this.data.caption || '', - }); + }) as HTMLElement; caption.dataset.placeholder = 'Enter a caption'; @@ -156,7 +324,7 @@ export default class SimpleImage { * @param {Element} blockContent - Tool's wrapper * @returns {SimpleImageData} */ - save(blockContent) { + save(blockContent: Element): SimpleImageData { const image = blockContent.querySelector('img'), caption = blockContent.querySelector('.' + this.CSS.input); @@ -166,7 +334,7 @@ export default class SimpleImage { return Object.assign(this.data, { url: image.src, - caption: caption.innerHTML, + caption: caption?.innerHTML || "", }); } @@ -190,7 +358,7 @@ export default class SimpleImage { * * @returns {boolean} */ - static get isReadOnlySupported() { + static get isReadOnlySupported(): boolean { return true; } @@ -201,18 +369,21 @@ export default class SimpleImage { * @param {File} file * @returns {Promise} */ - onDropHandler(file) { + onDropHandler(file: File): Promise { const reader = new FileReader(); reader.readAsDataURL(file); - return new Promise(resolve => { + return new Promise((resolve) => { reader.onload = (event) => { - resolve({ - url: event.target.result, - caption: file.name, - }); - }; + const target = event.target as FileReader; + if(typeof target.result === "string"){ + resolve({ + url: target.result, + caption: file.name, + }); + } + } }); } @@ -221,28 +392,29 @@ export default class SimpleImage { * * @param {PasteEvent} event - event with pasted config */ - onPaste(event) { + onPaste(event: PasteEvent) { switch (event.type) { case 'tag': { - const img = event.detail.data; - - this.data = { - url: img.src, - }; + const img = (event as HTMLPasteEvent).detail.data; + if (img instanceof HTMLImageElement) { + this.data = { + url: img.src + } as SimpleImageData; + } break; } case 'pattern': { - const { data: text } = event.detail; + const { data: text } = (event as PatternPasteEvent).detail; this.data = { - url: text, - }; + url: text + } as SimpleImageData; break; } case 'file': { - const { file } = event.detail; + const { file } = (event as FilePasteEvent).detail; this.onDropHandler(file) .then(data => { @@ -259,7 +431,7 @@ export default class SimpleImage { * * @returns {SimpleImageData} */ - get data() { + get data(): SimpleImageData { return this._data; } @@ -268,7 +440,7 @@ export default class SimpleImage { * * @param {SimpleImageData} data */ - set data(data) { + set data(data: SimpleImageData) { this._data = Object.assign({}, this.data, data); if (this.nodes.image) { @@ -302,12 +474,7 @@ export default class SimpleImage { }; } - /** - * Returns image tunes config - * - * @returns {Array} - */ - renderSettings() { + renderSettings(): Array { return this.tunes.map(tune => ({ ...tune, label: this.api.i18n.t(tune.label), @@ -325,7 +492,9 @@ export default class SimpleImage { * @param {object} attributes - any attributes * @returns {Element} */ - _make(tagName, classNames = null, attributes = {}) { + + + _make(tagName: string, classNames?: string[] | string, attributes: object = {}): HTMLElement | HTMLImageElement{ const el = document.createElement(tagName); if (Array.isArray(classNames)) { @@ -337,7 +506,6 @@ export default class SimpleImage { for (const attrName in attributes) { el[attrName] = attributes[attrName]; } - return el; } @@ -347,11 +515,12 @@ export default class SimpleImage { * @private * @param tune */ - _toggleTune(tune) { + _toggleTune(tune:string) { this.data[tune] = !this.data[tune]; this._acceptTuneView(); } + /** * Add specified class corresponds with activated tunes * @@ -359,8 +528,9 @@ export default class SimpleImage { */ _acceptTuneView() { this.tunes.forEach(tune => { - this.nodes.imageHolder.classList.toggle(this.CSS.imageHolder + '--' + tune.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`), !!this.data[tune.name]); - + if(this.nodes.imageHolder){ + this.nodes.imageHolder.classList.toggle(this.CSS.imageHolder + '--' + tune.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`), !!this.data[tune.name]); + } if (tune.name === 'stretched') { this.api.blocks.stretchBlock(this.blockIndex, !!this.data.stretched); } diff --git a/yarn.lock b/yarn.lock index c0a51d7..d031126 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,9 +2,15 @@ # yarn lockfile v1 -"@codexteam/icons@^0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.6.tgz#5553ada48dddf5940851ccc142cfe17835c36ad3" +"@codexteam/icons@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.3.2.tgz#b7aed0ba7b344e07953101f5476cded570d4f150" + integrity sha512-P1ep2fHoy0tv4wx85eic+uee5plDnZQ1Qa6gDfv7eHPkCXorMtVqJhzMb75o1izogh6G7380PqmFDXV3bW3Pig== + +"@editorjs/editorjs@^2.30.5": + version "2.30.5" + resolved "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.30.5.tgz" + integrity sha512-sE7m/UPbuf+nSGjv9cmWggFsfvtYlgEX7PCby2lZWvOsOLbRxuLT+ZYlwbWshD+8BFJwiAmBj9e+ScZcOjCzeg== "@esbuild/android-arm64@0.18.20": version "0.18.20" @@ -113,12 +119,12 @@ "@esbuild/win32-x64@0.18.20": version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz" integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== esbuild@^0.18.10: version "0.18.20" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz" integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== optionalDependencies: "@esbuild/android-arm" "0.18.20" @@ -151,17 +157,17 @@ fsevents@~2.3.2: nanoid@^3.3.6: version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== picocolors@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== postcss@^8.4.27: version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" @@ -170,24 +176,29 @@ postcss@^8.4.27: rollup@^3.27.1: version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + resolved "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz" integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== optionalDependencies: fsevents "~2.3.2" source-map-js@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + vite-plugin-css-injected-by-js@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.3.0.tgz#c19480a9e42a95c5bced976a9dde1446f9bd91ff" + resolved "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.3.0.tgz" integrity sha512-xG+jyHNCmUqi/TXp6q88wTJGeAOrNLSyUUTp4qEQ9QZLGcHWQQsCsSSKa59rPMQr8sOzfzmWDd8enGqfH/dBew== vite@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26" + resolved "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz" integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== dependencies: esbuild "^0.18.10"