Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (migration to typescript): transform javascript to typescript #128

Merged
merged 3 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
dependentmadani marked this conversation as resolved.
Show resolved Hide resolved
/** 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
Loading