Skip to content

Commit

Permalink
Merge pull request #128 from dependentmadani/migrate-typescript
Browse files Browse the repository at this point in the history
feat (migration to typescript): transform javascript to typescript
  • Loading branch information
neSpecc committed Jun 17, 2024
2 parents e3922a4 + ecaa2e2 commit f2585ab
Show file tree
Hide file tree
Showing 8 changed files with 696 additions and 76 deletions.
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@editorjs/embed",
"version": "2.7.2",
"version": "2.7.4",
"keywords": [
"codex editor",
"embed",
Expand All @@ -15,6 +15,7 @@
],
"main": "./dist/embed.umd.js",
"module": "./dist/embed.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/embed.mjs",
Expand All @@ -24,7 +25,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "mocha -r @babel/register -r ignore-styles --recursive ./test",
"test": "mocha --require ts-node/register --require ignore-styles --recursive './test/**/*.ts'",
"lint": "eslint src/ --ext .js",
"lint:errors": "eslint src/ --ext .js --quiet",
"lint:fix": "eslint src/ --ext .js --fix"
Expand All @@ -38,6 +39,10 @@
"@babel/plugin-transform-runtime": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/register": "^7.22.15",
"@types/chai": "^4.3.16",
"@types/debounce": "^1.2.4",
"@types/mocha": "^10.0.6",
"@types/node": "^20.14.2",
"chai": "^4.2.0",
"debounce": "^1.2.0",
"eslint": "^7.25.0",
Expand All @@ -46,8 +51,13 @@
"mocha": "^7.1.1",
"postcss-nested": "^4.2.1",
"postcss-nested-ancestors": "^2.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0"
"vite-plugin-css-injected-by-js": "^3.3.0",
"vite-plugin-dts": "^3.9.1"
},
"dependencies": {}
"dependencies": {
"@editorjs/editorjs": "^2.29.1"
}
}
149 changes: 99 additions & 50 deletions src/index.js → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,65 @@
import SERVICES from './services';
import './index.css';
import { debounce } from 'debounce';
import { ServiceConfig, ServicesConfigType } from './types/types';
import { API , PatternPasteEventDetail } from '@editorjs/editorjs';

/**
* @typedef {object} EmbedData
* @description Embed Tool data
* @property {string} service - service name
* @property {string} url - source URL of embedded content
* @property {string} embed - URL to source embed page
* @property {number} [width] - embedded content width
* @property {number} [height] - embedded content height
* @property {string} [caption] - content caption
*/
export interface EmbedData {
/** Service name */
service: string;
/** Source URL of embedded content */
source: string;
/** URL to source embed page */
embed: string;
/** Embedded content width */
width?: number;
/** Embedded content height */
height?: number;
/** Content caption */
caption?: string;
}

/**
* @typedef {object} PasteEvent
* @typedef {object} HTMLElement
* @typedef {object} Service
* @description Service configuration object
* @property {RegExp} regex - pattern of source URLs
* @property {string} embedUrl - URL scheme to embedded page. Use '<%= remote_id %>' to define a place to insert resource id
* @property {string} html - iframe which contains embedded content
* @property {Function} [id] - function to get resource id from RegExp groups
* @description Embed tool configuration object
*/
interface EmbedConfig {
/** Additional services provided by user */
services?: ServicesConfigType;
}

/**
* @typedef {object} EmbedConfig
* @description Embed tool configuration object
* @property {object} [services] - additional services provided by user. Each property should contain Service object
* @description CSS object
*/
interface CSS {
/** Base class for CSS */
baseClass: string;
/** CSS class for input */
input: string;
/** CSS class for container */
container: string;
/** CSS class for loading container */
containerLoading: string;
/** CSS class for preloader */
preloader: string;
/** CSS class for caption */
caption: string;
/** CSS class for URL */
url: string;
/** CSS class for content */
content: string;
}

interface ConstructorArgs {
// data — previously saved data
data: EmbedData;
// api - Editor.js API
api: API;
// readOnly - read-only mode flag
readOnly: boolean;
}

/**
* @class Embed
Expand All @@ -40,16 +73,28 @@ import { debounce } from 'debounce';
* @property {object} patterns - static property with patterns for paste handling configuration
*/
export default class Embed {
/** Editor.js API */
private api: API;
/** Private property with Embed data */
private _data: EmbedData;
/** Embedded content container */
private element: HTMLElement | null;
/** Read-only mode flag */
private readOnly: boolean;
/** Static property with available services */
static services: { [key: string]: ServiceConfig };
/** Static property with patterns for paste handling configuration */
static patterns: { [key: string]: RegExp };
/**
* @param {{data: EmbedData, config: EmbedConfig, api: object}}
* data — previously saved data
* config - user config for Tool
* api - Editor.js API
* readOnly - read-only mode flag
*/
constructor({ data, api, readOnly }) {
constructor({ data, api, readOnly }: ConstructorArgs) {
this.api = api;
this._data = {};
this._data = {} as EmbedData;
this.element = null;
this.readOnly = readOnly;

Expand All @@ -65,7 +110,7 @@ export default class Embed {
* @param {number} [data.width] - iframe width
* @param {string} [data.caption] - caption
*/
set data(data) {
set data(data: EmbedData) {
if (!(data instanceof Object)) {
throw Error('Embed Tool data should be object');
}
Expand All @@ -84,16 +129,16 @@ export default class Embed {
const oldView = this.element;

if (oldView) {
oldView.parentNode.replaceChild(this.render(), oldView);
oldView.parentNode?.replaceChild(this.render(), oldView);
}
}

/**
* @returns {EmbedData}
*/
get data() {
get data(): EmbedData {
if (this.element) {
const caption = this.element.querySelector(`.${this.api.styles.input}`);
const caption = this.element.querySelector(`.${this.api.styles.input}`) as HTMLElement;

this._data.caption = caption ? caption.innerHTML : '';
}
Expand All @@ -106,7 +151,7 @@ export default class Embed {
*
* @returns {object}
*/
get CSS() {
get CSS(): CSS {
return {
baseClass: this.api.styles.block,
input: this.api.styles.input,
Expand All @@ -124,7 +169,7 @@ export default class Embed {
*
* @returns {HTMLElement}
*/
render() {
render(): HTMLElement {
if (!this.data.service) {
const container = document.createElement('div');

Expand All @@ -144,17 +189,19 @@ export default class Embed {

container.appendChild(preloader);

caption.contentEditable = !this.readOnly;
caption.contentEditable = (!this.readOnly).toString();
caption.dataset.placeholder = this.api.i18n.t('Enter a caption');
caption.innerHTML = this.data.caption || '';

template.innerHTML = html;
template.content.firstChild.setAttribute('src', this.data.embed);
template.content.firstChild.classList.add(this.CSS.content);
(template.content.firstChild as HTMLElement).setAttribute('src', this.data.embed);
(template.content.firstChild as HTMLElement).classList.add(this.CSS.content);

const embedIsReady = this.embedIsReady(container);

container.appendChild(template.content.firstChild);
if (template.content.firstChild) {
container.appendChild(template.content.firstChild);
}
container.appendChild(caption);

embedIsReady
Expand All @@ -172,7 +219,7 @@ export default class Embed {
*
* @returns {HTMLElement}
*/
createPreloader() {
createPreloader(): HTMLElement {
const preloader = document.createElement('preloader');
const url = document.createElement('div');

Expand All @@ -191,7 +238,7 @@ export default class Embed {
*
* @returns {EmbedData}
*/
save() {
save(): EmbedData {
return this.data;
}

Expand All @@ -200,12 +247,12 @@ export default class Embed {
*
* @param {PasteEvent} event - event with pasted data
*/
onPaste(event) {
onPaste(event: { detail: PatternPasteEventDetail }) {
const { key: service, data: url } = event.detail;

const { regex, embedUrl, width, height, id = (ids) => ids.shift() } = Embed.services[service];
const result = regex.exec(url).slice(1);
const embed = embedUrl.replace(/<%= remote_id %>/g, id(result));
const { regex, embedUrl, width, height, id = (ids) => ids.shift() || '' } = Embed.services[service];
const result = regex.exec(url)?.slice(1);
const embed = result ? embedUrl.replace(/<%= remote_id %>/g, id(result)) : '';

this.data = {
service,
Expand All @@ -221,7 +268,7 @@ export default class Embed {
*
* @param {EmbedConfig} config - configuration of embed block element
*/
static prepare({ config = {} }) {
static prepare({ config = {} } : {config: EmbedConfig}) {
const { services = {} } = config;

let entries = Object.entries(SERVICES);
Expand All @@ -238,9 +285,9 @@ export default class Embed {
.filter(([key, value]) => {
return typeof value === 'object';
})
.filter(([key, service]) => Embed.checkServiceConfig(service))
.filter(([key, service]) => Embed.checkServiceConfig(service as ServiceConfig))
.map(([key, service]) => {
const { regex, embedUrl, html, height, width, id } = service;
const { regex, embedUrl, html, height, width, id } = service as ServiceConfig;

return [key, {
regex,
Expand All @@ -249,7 +296,7 @@ export default class Embed {
height,
width,
id,
} ];
} ] as [string, ServiceConfig];
});

if (enabledServices.length) {
Expand All @@ -258,9 +305,9 @@ export default class Embed {

entries = entries.concat(userServices);

Embed.services = entries.reduce((result, [key, service]) => {
Embed.services = entries.reduce<{ [key: string]: ServiceConfig }>((result, [key, service]) => {
if (!(key in result)) {
result[key] = service;
result[key] = service as ServiceConfig;

return result;
}
Expand All @@ -271,8 +318,10 @@ export default class Embed {
}, {});

Embed.patterns = entries
.reduce((result, [key, item]) => {
result[key] = item.regex;
.reduce<{ [key: string]: RegExp }>((result, [key, item]) => {
if (item && typeof item !== 'boolean') {
result[key] = (item as ServiceConfig).regex as RegExp;
}

return result;
}, {});
Expand All @@ -284,12 +333,12 @@ export default class Embed {
* @param {Service} config - configuration of embed block element
* @returns {boolean}
*/
static checkServiceConfig(config) {
static checkServiceConfig(config: ServiceConfig): boolean {
const { regex, embedUrl, html, height, width, id } = config;

let isValid = regex && regex instanceof RegExp &&
embedUrl && typeof embedUrl === 'string' &&
html && typeof html === 'string';
let isValid = Boolean(regex && regex instanceof RegExp) &&
Boolean(embedUrl && typeof embedUrl === 'string') &&
Boolean(html && typeof html === 'string');

isValid = isValid && (id !== undefined ? id instanceof Function : true);
isValid = isValid && (height !== undefined ? Number.isFinite(height) : true);
Expand Down Expand Up @@ -324,10 +373,10 @@ export default class Embed {
* @param {HTMLElement} targetNode - HTML-element mutations of which to listen
* @returns {Promise<any>} - result that all mutations have finished
*/
embedIsReady(targetNode) {
embedIsReady(targetNode: HTMLElement): Promise<void> {
const PRELOADER_DELAY = 450;

let observer = null;
let observer: MutationObserver;

return new Promise((resolve, reject) => {
observer = new MutationObserver(debounce(resolve, PRELOADER_DELAY));
Expand Down
Loading

0 comments on commit f2585ab

Please sign in to comment.