diff --git a/src/interface/components/textEditor/constants.ts b/src/interface/components/textEditor/constants.ts deleted file mode 100644 index caf4de7f..00000000 --- a/src/interface/components/textEditor/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -const METHOD = { - GET: 'get', - DELETE: 'delete', - POST: 'post', - PUT: 'put', -} as const - -type ObjectValues = T[keyof T] -type Method = ObjectValues - -export default Method \ No newline at end of file diff --git a/src/interface/components/textEditor/interfaces.ts b/src/interface/components/textEditor/interfaces.ts deleted file mode 100644 index b501908e..00000000 --- a/src/interface/components/textEditor/interfaces.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { ResponseFn } from './sbFetch' - -export interface ISbStoriesParams - extends Partial, - ISbMultipleStoriesData { - resolve_level?: number - _stopResolving?: boolean - by_slugs?: string - by_uuids?: string - by_uuids_ordered?: string - component?: string - content_type?: string - cv?: number - datasource?: string - dimension?: string - excluding_fields?: string - excluding_ids?: string - excluding_slugs?: string - fallback_lang?: string - filename?: string - filter_query?: any - first_published_at_gt?: string - first_published_at_lt?: string - from_release?: string - is_startpage?: boolean - language?: string - level?: number - page?: number - per_page?: number - published_at_gt?: string - published_at_lt?: string - resolve_assets?: number - resolve_links?: 'link' | 'url' | 'story' | '0' | '1' | 'link' - resolve_relations?: string | string[] - search_term?: string - size?: string - sort_by?: string - starts_with?: string - token?: string - version?: 'draft' | 'published' - with_tag?: string -} - -export interface ISbStoryParams { - resolve_level?: number - token?: string - find_by?: 'uuid' - version?: 'draft' | 'published' - resolve_links?: 'link' | 'url' | 'story' | '0' | '1' - resolve_relations?: string | string[] - cv?: number - from_release?: string - language?: string - fallback_lang?: string -} - -export interface ISbComponentType { - _uid?: string - component?: T - _editable?: string -} - -export interface ISbStoryData< - Content = ISbComponentType & { [index: string]: any }, -> extends ISbMultipleStoriesData { - alternates: ISbAlternateObject[] - breadcrumbs?: ISbLinkURLObject[] - content: Content - created_at: string - default_full_slug?: string - default_root?: string - disble_fe_editor?: boolean - first_published_at?: string - full_slug: string - group_id: string - id: number - imported_at?: string - is_folder?: boolean - is_startpage?: boolean - lang: string - last_author?: { - id: number - userid: string - } - meta_data: any - name: string - parent?: ISbStoryData - parent_id: number - path?: string - pinned?: '1' | boolean - position: number - published?: boolean - published_at: string | null - release_id?: number - slug: string - sort_by_date: string | null - tag_list: string[] - translated_slugs?: { - path: string - name: string | null - lang: ISbStoryData['lang'] - }[] - unpublished_changes?: boolean - updated_at?: string - uuid: string -} - -export interface ISbMultipleStoriesData { - by_ids?: string - by_uuids?: string - contain_component?: string - excluding_ids?: string - filter_query?: any - folder_only?: boolean - full_slug?: string - in_release?: string - in_trash?: boolean - is_published?: boolean - in_workflow_stages?: string - page?: number - pinned?: '1' | boolean - search?: string - sort_by?: string - starts_with?: string - story_only?: boolean - text_search?: string - with_parent?: number - with_slug?: string - with_tag?: string -} - -export interface ISbAlternateObject { - id: number - name: string - slug: string - published: boolean - full_slug: string - is_folder: boolean - parent_id: number -} - -export interface ISbLinkURLObject { - id: number - name: string - slug: string - full_slug: string - url: string - uuid: string -} - -export interface ISbStories { - data: { - cv: number - links: (ISbStoryData | ISbLinkURLObject)[] - rels: ISbStoryData[] - stories: ISbStoryData[] - } - perPage: number - total: number - headers: any -} - -export interface ISbStory { - data: { - cv: number - links: (ISbStoryData | ISbLinkURLObject)[] - rels: ISbStoryData[] - story: ISbStoryData - } - headers: any -} - -export interface IMemoryType extends ISbResult { - [key: string]: any -} - -export interface ICacheProvider { - get: (key: string) => Promise - set: (key: string, content: ISbResult) => Promise - getAll: () => Promise - flush: () => Promise -} - -export interface ISbCache { - type?: 'none' | 'memory' | 'custom' - clear?: 'auto' | 'manual' - custom?: ICacheProvider -} - -export interface ISbConfig { - accessToken?: string - oauthToken?: string - resolveNestedRelations?: boolean - cache?: ISbCache - responseInterceptor?: ResponseFn - fetch?: typeof fetch - timeout?: number - headers?: any - region?: string - maxRetries?: number - https?: boolean - rateLimit?: number - componentResolver?: (component: string, data: any) => void - richTextSchema?: ISbSchema - endpoint?: string -} - -export interface ISbResult { - data: any - perPage: number - total: number - headers: Headers -} - -export interface ISbResponse { - data: any - status: number - statusText: string -} - -export interface ISbError { - message?: string - status?: number - response?: ISbResponse -} - -export interface ISbNode extends Element { - content: object[] - attrs: { - anchor?: string - body?: Array> - href?: string - level?: number - linktype?: string - custom?: LinkCustomAttributes - [key: string]: any | undefined - } -} - -export type NodeSchema = { - (node: ISbNode): object -} - -export type MarkSchema = { - (node: ISbNode): object -} - -export interface ISbContentMangmntAPI< - Content = ISbComponentType & { [index: string]: any }, -> { - story: { - name: string - slug: string - content?: Content - default_root?: boolean - is_folder?: boolean - parent_id?: string - disble_fe_editor?: boolean - path?: string - is_startpage?: boolean - position?: number - first_published_at?: string - translated_slugs_attributes?: { - path: string - name: string | null - lang: ISbContentMangmntAPI['lang'] - }[] - } - force_update?: '1' | unknown - release_id?: number - publish?: '1' | unknown - lang?: string -} - -export interface ISbManagmentApiResult { - data: any - headers: any -} - -export interface ISbSchema { - nodes: any - marks: any -} - -export interface ISbRichtext { - content?: ISbRichtext[] - marks?: ISbRichtext[] - attrs?: any - text?: string - type: string -} - -export interface LinkCustomAttributes { - rel?: string - title?: string - [key: string]: any -} - -export interface ISbLink { - id?: number - slug?: string - name?: string - is_folder?: boolean - parent_id?: number - published?: boolean - position?: number - uuid?: string - is_startpage?: boolean -} - -export interface ISbLinks { - links?: { - [key: string]: ISbLink - } -} - -export type ThrottleFn = { - (...args: any): any -} - -export type AsyncFn = (...args: any) => [] | Promise - -export type ArrayFn = (...args: any) => void - -export type HtmlEscapes = { - [key: string]: string -} - -export interface ISbCustomFetch extends Omit {} \ No newline at end of file diff --git a/src/interface/components/textEditor/richTextResolver.ts b/src/interface/components/textEditor/richTextResolver.ts deleted file mode 100644 index 91f7a2f8..00000000 --- a/src/interface/components/textEditor/richTextResolver.ts +++ /dev/null @@ -1,396 +0,0 @@ -import defaultHtmlSerializer from './schema' -import { ISbSchema, ISbRichtext } from './interfaces' - -type HtmlEscapes = { - [key: string]: string -} - -type OptimizeImagesOptions = - | boolean - | { - class?: string - filters?: { - blur?: number - brightness?: number - fill?: string - format?: 'webp' | 'jpeg' | 'png' - grayscale?: boolean - quality?: number - rotate?: 90 | 180 | 270 - } - height?: number - loading?: 'lazy' | 'eager' - sizes?: string[] - srcset?: (number | [number, number])[] - width?: number - } - -type RenderOptions = { - optimizeImages?: OptimizeImagesOptions -} - -const escapeHTML = function (string: string) { - const htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - } as HtmlEscapes - - const reUnescapedHtml = /[&<>"']/g - const reHasUnescapedHtml = RegExp(reUnescapedHtml.source) - - return string && reHasUnescapedHtml.test(string) - ? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr]) - : string -} - -interface ISbTag extends Element { - [key: string]: any -} - -interface ISbNode { - [key: string]: ISbSchema | ((arg: ISbRichtext) => any) -} - -interface ISbFunction { - (...args: T): R -} - -class RichTextResolver { - private marks: ISbNode - private nodes: ISbNode - - public constructor(schema?: ISbSchema) { - if (!schema) { - schema = defaultHtmlSerializer as ISbSchema - } - - this.marks = schema.marks || [] - this.nodes = schema.nodes || [] - } - - public addNode(key: string, schema: ISbSchema | ISbFunction) { - this.nodes[key] = schema - } - - public addMark(key: string, schema: ISbSchema) { - this.marks[key] = schema - } - - public render( - data?: ISbRichtext, - options: RenderOptions = { optimizeImages: false } - ) { - if (data && data.content && Array.isArray(data.content)) { - let html = '' - - data.content.forEach((node: any) => { - html += this.renderNode(node) - }) - - if (options.optimizeImages) { - return this.optimizeImages(html, options.optimizeImages) - } - - return html - } - - console.warn( - `The render method must receive an Object with a "content" field. - The "content" field must be an array of nodes as the type ISbRichtext. - ISbRichtext: - content?: ISbRichtext[] - marks?: ISbRichtext[] - attrs?: any - text?: string - type: string - - Example: - { - content: [ - { - content: [ - { - text: 'Hello World', - type: 'text' - } - ], - type: 'paragraph' - } - ], - type: 'doc' - }` - ) - return '' - } - - private optimizeImages(html: string, options: OptimizeImagesOptions): string { - let w = 0 - let h = 0 - let imageAttributes = '' - let filters = '' - - if (typeof options !== 'boolean') { - if (typeof options.width === 'number' && options.width > 0) { - imageAttributes += `width="${options.width}" ` - w = options.width - } - - if (typeof options.height === 'number' && options.height > 0) { - imageAttributes += `height="${options.height}" ` - h = options.height - } - - if (options.loading === 'lazy' || options.loading === 'eager') { - imageAttributes += `loading="${options.loading}" ` - } - - if (typeof options.class === 'string' && options.class.length > 0) { - imageAttributes += `class="${options.class}" ` - } - - if (options.filters) { - if ( - typeof options.filters.blur === 'number' && - options.filters.blur >= 0 && - options.filters.blur <= 100 - ) { - filters += `:blur(${options.filters.blur})` - } - - if ( - typeof options.filters.brightness === 'number' && - options.filters.brightness >= -100 && - options.filters.brightness <= 100 - ) { - filters += `:brightness(${options.filters.brightness})` - } - - if ( - options.filters.fill && - (options.filters.fill.match(/[0-9A-Fa-f]{6}/g) || - options.filters.fill === 'transparent') - ) { - filters += `:fill(${options.filters.fill})` - } - - if ( - options.filters.format && - ['webp', 'png', 'jpeg'].includes(options.filters.format) - ) { - filters += `:format(${options.filters.format})` - } - - if ( - typeof options.filters.grayscale === 'boolean' && - options.filters.grayscale - ) { - filters += ':grayscale()' - } - - if ( - typeof options.filters.quality === 'number' && - options.filters.quality >= 0 && - options.filters.quality <= 100 - ) { - filters += `:quality(${options.filters.quality})` - } - - if ( - options.filters.rotate && - [90, 180, 270].includes(options.filters.rotate) - ) { - filters += `:rotate(${options.filters.rotate})` - } - - if (filters.length > 0) filters = '/filters' + filters - } - } - - if (imageAttributes.length > 0) { - html = html.replace(/ { - const url = value.match( - /a.storyblok.com\/f\/(\d+)\/([^.]+)\.(gif|jpg|jpeg|png|tif|tiff|bmp)/g - ) - - if (url && url.length > 0) { - const imageAttributes = { - srcset: options.srcset - ?.map((value) => { - if (typeof value === 'number') { - return `//${url}/m/${value}x0${filters} ${value}w` - } - - if (typeof value === 'object' && value.length === 2) { - let w = 0 - let h = 0 - if (typeof value[0] === 'number') w = value[0] - if (typeof value[1] === 'number') h = value[1] - return `//${url}/m/${w}x${h}${filters} ${w}w` - } - }) - .join(', '), - sizes: options.sizes?.map((size) => size).join(', '), - } - - let renderImageAttributes = '' - if (imageAttributes.srcset) { - renderImageAttributes += `srcset="${imageAttributes.srcset}" ` - } - if (imageAttributes.sizes) { - renderImageAttributes += `sizes="${imageAttributes.sizes}" ` - } - - return value.replace(/ { - const mark = this.getMatchingMark(m) - - if (mark && mark.tag !== '') { - html.push(this.renderOpeningTag(mark.tag)) - } - }) - } - - const node = this.getMatchingNode(item) - - if (node && node.tag) { - html.push(this.renderOpeningTag(node.tag)) - } - - if (item.content) { - item.content.forEach((content: any) => { - html.push(this.renderNode(content)) - }) - } else if (item.text) { - html.push(escapeHTML(item.text)) - } else if (node && node.singleTag) { - html.push(this.renderTag(node.singleTag, ' /')) - } else if (node && node.html) { - html.push(node.html) - } else if (item.type === 'emoji') { - html.push(this.renderEmoji(item)) - } - - if (node && node.tag) { - html.push(this.renderClosingTag(node.tag)) - } - - if (item.marks) { - item.marks - .slice(0) - .reverse() - .forEach((m) => { - const mark = this.getMatchingMark(m) - - if (mark && mark.tag !== '') { - html.push(this.renderClosingTag(mark.tag)) - } - }) - } - - return html.join('') - } - - private renderTag(tags: ISbTag[], ending: string) { - if (tags.constructor === String) { - return `<${tags}${ending}>` - } - - const all = tags.map((tag) => { - if (tag.constructor === String) { - return `<${tag}${ending}>` - } else { - let h = `<${tag.tag}` - if (tag.attrs) { - for (const key in tag.attrs) { - const value = tag.attrs[key] - if (value !== null) { - h += ` ${key}="${value}"` - } - } - } - - return `${h}${ending}>` - } - }) - return all.join('') - } - - private renderOpeningTag(tags: ISbTag[]) { - return this.renderTag(tags, '') - } - - private renderClosingTag(tags: ISbTag[]) { - if (tags.constructor === String) { - return `` - } - - const all = tags - .slice(0) - .reverse() - .map((tag) => { - if (tag.constructor === String) { - return `` - } else { - return `` - } - }) - - return all.join('') - } - - private getMatchingNode(item: ISbRichtext) { - const node = this.nodes[item.type] - if (typeof node === 'function') { - return node(item) - } - } - - private getMatchingMark(item: ISbRichtext) { - const node = this.marks[item.type] - if (typeof node === 'function') { - return node(item) - } - } - - private renderEmoji(item: ISbRichtext) { - if (item.attrs.emoji) { - return item.attrs.emoji - } - - const emojiImageContainer = [ - { - tag: 'img', - attrs: { - src: item.attrs.fallbackImage, - draggable: 'false', - loading: 'lazy', - align: 'absmiddle', - }, - }, - ] as unknown as ISbTag[] - - return this.renderTag(emojiImageContainer, ' /') - } -} - -export default RichTextResolver \ No newline at end of file diff --git a/src/interface/components/textEditor/sbFetch.ts b/src/interface/components/textEditor/sbFetch.ts deleted file mode 100644 index dd67431f..00000000 --- a/src/interface/components/textEditor/sbFetch.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { SbHelpers } from './sbHelpers' - -import { - ISbResponse, - ISbError, - ISbStoriesParams, - ISbCustomFetch, -} from './interfaces' -import Method from './constants' - -export type ResponseFn = { - (arg?: ISbResponse | any): any -} - -interface ISbFetch { - baseURL: string - timeout?: number - headers: Headers - responseInterceptor?: ResponseFn - fetch?: typeof fetch -} - -class SbFetch { - private baseURL: string - private timeout?: number - private headers: Headers - private responseInterceptor?: ResponseFn - private fetch: typeof fetch - private ejectInterceptor?: boolean - private url: string - private parameters: ISbStoriesParams - private fetchOptions: ISbCustomFetch - - public constructor($c: ISbFetch) { - this.baseURL = $c.baseURL - this.headers = $c.headers || new Headers() - this.timeout = $c?.timeout ? $c.timeout * 1000 : 0 - this.responseInterceptor = $c.responseInterceptor - this.fetch = (...args: [any]) => - $c.fetch ? $c.fetch(...args) : fetch(...args) - this.ejectInterceptor = false - this.url = '' - this.parameters = {} as ISbStoriesParams - this.fetchOptions = {} - } - - /** - * - * @param url string - * @param params ISbStoriesParams - * @returns Promise - */ - public get(url: string, params: ISbStoriesParams) { - this.url = url - this.parameters = params - return this._methodHandler('get') - } - - public post(url: string, params: ISbStoriesParams) { - this.url = url - this.parameters = params - return this._methodHandler('post') - } - - public put(url: string, params: ISbStoriesParams) { - this.url = url - this.parameters = params - return this._methodHandler('put') - } - - public delete(url: string, params: ISbStoriesParams) { - this.url = url - this.parameters = params - return this._methodHandler('delete') - } - - private async _responseHandler(res: Response) { - const headers: string[] = [] - const response = { - data: {}, - headers: {}, - status: 0, - statusText: '', - } - - if (res.status !== 204) { - await res.json().then(($r) => { - response.data = $r - }) - } - - for (const pair of res.headers.entries()) { - headers[pair[0] as any] = pair[1] - } - - response.headers = { ...headers } - response.status = res.status - response.statusText = res.statusText - - return response - } - - private async _methodHandler( - method: Method - ): Promise { - let urlString = `${this.baseURL}${this.url}` - - let body = null - - if (method === 'get') { - const helper = new SbHelpers() - urlString = `${this.baseURL}${this.url}?${helper.stringify( - this.parameters - )}` - } else { - body = JSON.stringify(this.parameters) - } - - const url = new URL(urlString) - - const controller = new AbortController() - const { signal } = controller - - let timeout - - if (this.timeout) { - timeout = setTimeout(() => controller.abort(), this.timeout) - } - - try { - const fetchResponse = await this.fetch(`${url}`, { - method, - headers: this.headers, - body, - signal, - ...this.fetchOptions, - }) - - if (this.timeout) { - clearTimeout(timeout) - } - - const response = (await this._responseHandler( - fetchResponse - )) as ISbResponse - - if (this.responseInterceptor && !this.ejectInterceptor) { - return this._statusHandler(this.responseInterceptor(response)) - } else { - return this._statusHandler(response) - } - } catch (err: any) { - const error: ISbError = { - message: err, - } - return error - } - } - - public setFetchOptions(fetchOptions: ISbCustomFetch = {}) { - if (Object.keys(fetchOptions).length > 0 && 'method' in fetchOptions) { - delete fetchOptions.method - } - this.fetchOptions = { ...fetchOptions } - } - - public eject() { - this.ejectInterceptor = true - } - - private _statusHandler(res: ISbResponse): Promise { - const statusOk = /20[0-6]/g - - return new Promise((resolve, reject) => { - if (statusOk.test(`${res.status}`)) { - return resolve(res) - } - - const error: ISbError = { - message: res.statusText, - status: res.status, - response: Array.isArray(res.data) - ? res.data[0] - : res.data.error || res.data.slug, - } - - reject(error) - }) - } -} - -export default SbFetch \ No newline at end of file diff --git a/src/interface/components/textEditor/sbHelpers.ts b/src/interface/components/textEditor/sbHelpers.ts deleted file mode 100644 index b2a8ef8c..00000000 --- a/src/interface/components/textEditor/sbHelpers.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ISbStoriesParams, ISbResult, AsyncFn, HtmlEscapes } from './interfaces' -interface ISbParams extends ISbStoriesParams { - [key: string]: any -} - -type ArrayFn = (...args: any) => void - -type FlatMapFn = (...args: any) => [] | any - -type RangeFn = (...args: any) => [] - -export class SbHelpers { - public isCDNUrl = (url = '') => url.indexOf('/cdn/') > -1 - - public getOptionsPage = ( - options: ISbStoriesParams, - perPage = 25, - page = 1 - ) => { - return { - ...options, - per_page: perPage, - page, - } - } - - public delay = (ms: number) => new Promise((res) => setTimeout(res, ms)) - - public arrayFrom = (length = 0, func: ArrayFn) => [...Array(length)].map(func) - - public range = (start = 0, end = start): Array => { - const length = Math.abs(end - start) || 0 - const step = start < end ? 1 : -1 - return this.arrayFrom(length, (_, i: number) => i * step + start) - } - - public asyncMap = async (arr: RangeFn[], func: AsyncFn) => - Promise.all(arr.map(func)) - - public flatMap = (arr: ISbResult[] = [], func: FlatMapFn) => - arr.map(func).reduce((xs, ys) => [...xs, ...ys], []) - - /** - * @method stringify - * @param {Object} params - * @param {String} prefix - * @param {Boolean} isArray - * @return {String} Stringified object - */ - public stringify( - params: ISbParams, - prefix?: string, - isArray?: boolean - ): string { - const pairs = [] - for (const key in params) { - if (!Object.prototype.hasOwnProperty.call(params, key)) { - continue - } - const value = params[key] - const enkey = isArray ? '' : encodeURIComponent(key) - let pair - if (typeof value === 'object') { - pair = this.stringify( - value, - prefix ? prefix + encodeURIComponent('[' + enkey + ']') : enkey, - Array.isArray(value) - ) - } else { - pair = - (prefix ? prefix + encodeURIComponent('[' + enkey + ']') : enkey) + - '=' + - encodeURIComponent(value) - } - pairs.push(pair) - } - return pairs.join('&') - } - - /** - * @method escapeHTML - * @param {String} string text to be parsed - * @return {String} Text parsed - */ - public escapeHTML = function (string: string) { - const htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - } as HtmlEscapes - - const reUnescapedHtml = /[&<>"']/g - const reHasUnescapedHtml = RegExp(reUnescapedHtml.source) - - return string && reHasUnescapedHtml.test(string) - ? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr]) - : string - } -} \ No newline at end of file diff --git a/src/interface/components/textEditor/schema.ts b/src/interface/components/textEditor/schema.ts deleted file mode 100644 index 4efa83af..00000000 --- a/src/interface/components/textEditor/schema.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { ISbNode, NodeSchema, MarkSchema, ISbComponentType } from './interfaces' -import { SbHelpers } from './sbHelpers' - -const pick = function (attrs: Attrs, allowed: string[]) { - const h = {} as Attrs - - for (const key in attrs) { - const value = attrs[key] - if (allowed.indexOf(key) > -1 && value !== null) { - h[key] = value - } - } - return h -} - -const isEmailLinkType = (type: string) => type === 'email' - -type Attrs = { - [key: string]: string | number | Array> -} - -// nodes -const horizontal_rule: NodeSchema = () => { - return { - singleTag: 'hr', - } -} -const blockquote: NodeSchema = () => { - return { - tag: 'blockquote', - } -} -const bullet_list: NodeSchema = () => { - return { - tag: 'ul', - } -} -const code_block: NodeSchema = (node: ISbNode) => { - return { - tag: [ - 'pre', - { - tag: 'code', - attrs: node.attrs, - }, - ], - } -} -const hard_break: NodeSchema = () => { - return { - singleTag: 'br', - } -} -const heading: NodeSchema = (node: ISbNode) => { - return { - tag: `h${node.attrs.level}`, - } -} - -const image: NodeSchema = (node: ISbNode) => { - return { - singleTag: [ - { - tag: 'img', - attrs: pick(node.attrs, ['src', 'alt', 'title']), - }, - ], - } -} -const list_item: NodeSchema = () => { - return { - tag: 'li', - } -} -const ordered_list: NodeSchema = () => { - return { - tag: 'ol', - } -} -const paragraph: NodeSchema = () => { - return { - tag: 'p', - } -} - -const emoji: NodeSchema = (node: ISbNode) => { - const attrs = { - ['data-type']: 'emoji', - ['data-name']: node.attrs.name, - emoji: node.attrs.emoji - } - - return { - tag: [ - { - tag: 'span', - attrs: attrs, - }, - ], - } -} - -// marks -const bold: MarkSchema = () => { - return { - tag: 'b', - } -} -const strike: MarkSchema = () => { - return { - tag: 's', - } -} -const underline: MarkSchema = () => { - return { - tag: 'u', - } -} -const strong: MarkSchema = () => { - return { - tag: 'strong', - } -} -const code: MarkSchema = () => { - return { - tag: 'code', - } -} -const italic: MarkSchema = () => { - return { - tag: 'i', - } -} -const link: MarkSchema = (node: ISbNode) => { - if (!node.attrs) { - return { - tag: '', - } - } - const escapeHTML = new SbHelpers().escapeHTML - const attrs = { ...node.attrs } - const { linktype = 'url' } = node.attrs - delete attrs.linktype - - if (attrs.href) { - attrs.href = escapeHTML(node.attrs.href || '') - } - - if (isEmailLinkType(linktype)) { - attrs.href = `mailto:${attrs.href}` - } - - if (attrs.anchor) { - attrs.href = `${attrs.href}#${attrs.anchor}` - delete attrs.anchor - } - - if (attrs.custom) { - for (const key in attrs.custom) { - attrs[key] = attrs.custom[key] - } - delete attrs.custom - } - - return { - tag: [ - { - tag: 'a', - attrs: attrs, - }, - ], - } -} - -const styled: MarkSchema = (node: ISbNode) => { - return { - tag: [ - { - tag: 'span', - attrs: node.attrs, - }, - ], - } -} - -const subscript: MarkSchema = () => { - return { - tag: 'sub', - } -} - -const superscript: MarkSchema = () => { - return { - tag: 'sup' - } -} - -const anchor: MarkSchema = (node: ISbNode) => { - return { - tag: [ - { - tag: 'span', - attrs: node.attrs, - }, - ], - } -} - -const highlight: MarkSchema = (node: ISbNode) => { - if (!node.attrs?.color) return { - tag: '', - } - - const attrs = { - ['style']: `background-color:${node.attrs.color};`, - } - return { - tag: [ - { - tag: 'span', - attrs, - }, - ], - } -} - -const textStyle: MarkSchema = (node: ISbNode) => { - if (!node.attrs?.color) return { - tag: '', - } - - const attrs = { - ['style']: `color:${node.attrs.color}`, - } - return { - tag: [ - { - tag: 'span', - attrs, - }, - ], - } -} - -export default { - nodes: { - horizontal_rule, - blockquote, - bullet_list, - code_block, - hard_break, - heading, - image, - list_item, - ordered_list, - paragraph, - emoji - }, - marks: { - bold, - strike, - underline, - strong, - code, - italic, - link, - styled, - subscript, - superscript, - anchor, - highlight, - textStyle, - }, -} \ No newline at end of file diff --git a/src/interface/main.tsx b/src/interface/main.tsx index f59820db..26a7f50f 100644 --- a/src/interface/main.tsx +++ b/src/interface/main.tsx @@ -18,6 +18,7 @@ browser.storage.local.get().then(({ DarkMode }) => { const style = document.createElement("style"); style.setAttribute("type", "text/css"); +style.classList.add('iconFamily') style.innerHTML = ` @font-face { font-family: 'IconFamily'; diff --git a/src/interface/pages/Editor.css b/src/interface/pages/Editor.css index 13529f17..2a3b8994 100644 --- a/src/interface/pages/Editor.css +++ b/src/interface/pages/Editor.css @@ -8,4 +8,8 @@ body { .bn-container > [contenteditable="true"] { height: 100vh !important; +} + +.ProseMirror { + background: transparent !important; } \ No newline at end of file diff --git a/src/interface/pages/Editor.tsx b/src/interface/pages/Editor.tsx index b792a5db..918f7748 100644 --- a/src/interface/pages/Editor.tsx +++ b/src/interface/pages/Editor.tsx @@ -2,18 +2,16 @@ import "@blocknote/core/fonts/inter.css"; import { useCreateBlockNote } from "@blocknote/react"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; -//import { generateHTML } from '@tiptap/html' import './Editor.css' export default function Editor() { const editor = useCreateBlockNote({}); - /* debounce on change to export to html */ editor._tiptapEditor.on('update', () => { window.parent.postMessage({ type: 'message-html', - data: editor._tiptapEditor.getHTML() + data: editor._tiptapEditor.getHTML(), }, '*') }) diff --git a/src/seqta/ui/customMessageEditor.ts b/src/seqta/ui/customMessageEditor.ts index 5e2cd795..f88a9a4a 100644 --- a/src/seqta/ui/customMessageEditor.ts +++ b/src/seqta/ui/customMessageEditor.ts @@ -6,41 +6,66 @@ export default async function handleComposeMessage(): Promise { console.log('COMPOSE MESSAGE!'); const container: HTMLElement | null = document.querySelector('.pane .footer .pillbox'); - const firstButton: HTMLButtonElement | null = document.querySelector('.pane .footer .pillbox button.first') as HTMLButtonElement; + const simpleEditorButton: HTMLButtonElement | null = document.querySelector('.pane .footer .pillbox button.first') as HTMLButtonElement; - if (container && firstButton) { + if (container && simpleEditorButton) { const buttonHTML = /* html */ ` - `; const button: HTMLElement = stringToHTML(buttonHTML); - // Append the new button as the second child of options - firstButton.parentNode?.insertBefore(button.firstChild as Node, firstButton.nextSibling); + // Check if the button already exists + if (!container.querySelector('#betterEditorButton')) { + // Insert the new button after the Simple editor button + simpleEditorButton.insertAdjacentElement('afterend', button.firstElementChild as HTMLElement); + } - // Add click event listeners to both buttons - container.addEventListener('click', (event: Event) => handleButtonClick(event, container, firstButton)); + // Add click event listeners to the container (event delegation) + container.addEventListener('click', handleButtonClick); } } -function handleButtonClick(event: Event, container: HTMLElement, firstButton: HTMLButtonElement): void { +function handleButtonClick(event: MouseEvent): void { + console.log('handleButtonClick', event); const target = event.target as HTMLElement; - if (target && target.classList.contains('button')) { - const isBetterEditorButton = target.textContent?.trim() === 'Better Editor'; + if (target.tagName !== 'BUTTON') return; - if (isBetterEditorButton) { - activateBetterEditor(container, firstButton); - } else { - deactivateBetterEditor(container, firstButton); - } + const container = target.closest('.pillbox') as HTMLElement; + if (!container) return; + + const simpleEditorButton = container.querySelector('button.first') as HTMLButtonElement; + const betterEditorButton = container.querySelector('#betterEditorButton') as HTMLButtonElement; + + if (!simpleEditorButton || !betterEditorButton) { + console.error('Could not find Simple Editor or Better Editor buttons'); + return; } + + const isBetterEditorButton = target === betterEditorButton; + const isSimpleEditorButton = target === simpleEditorButton; + + if (isBetterEditorButton) { + activateBetterEditor(simpleEditorButton, betterEditorButton); + } else if (isSimpleEditorButton) { + activateSimpleEditor(simpleEditorButton, betterEditorButton); + } else { + deactivateBetterEditor(simpleEditorButton, betterEditorButton); + } + + container.querySelectorAll('button').forEach(btn => btn.classList.remove('depressed')); + target.classList.add('depressed'); } -function activateBetterEditor(container: HTMLElement, firstButton: HTMLButtonElement): void { - firstButton.classList.remove('depressed'); - container.children[1]?.classList.add('depressed'); +function activateBetterEditor(simpleEditorButton: HTMLButtonElement, betterEditorButton: HTMLButtonElement): void { + // Programmatically click the Simple Editor button first + simpleEditorButton.click(); + + // Then proceed with Better Editor activation + simpleEditorButton.classList.remove('depressed'); + betterEditorButton.classList.add('depressed'); const ckeInner = document.querySelector('.pane .cke_inner') as HTMLElement; if (ckeInner) ckeInner.style.display = 'none'; @@ -63,23 +88,33 @@ function activateBetterEditor(container: HTMLElement, firstButton: HTMLButtonEle window.addEventListener('message', (event) => handleEditorMessage(event, ckeEditor), { once: true }); } -function deactivateBetterEditor(container: HTMLElement, firstButton: HTMLButtonElement): void { +function activateSimpleEditor(simpleEditorButton: HTMLButtonElement, betterEditorButton: HTMLButtonElement): void { + simpleEditorButton.classList.add('depressed'); + betterEditorButton.classList.remove('depressed'); + const ckeInner = document.querySelector('.pane .cke_inner') as HTMLElement; const extensionEditor = document.querySelector('.extension-editor') as HTMLIFrameElement; - if (ckeInner && extensionEditor) { - ckeInner.style.display = 'block'; - firstButton.classList.add('depressed'); - container.children[1]?.classList.remove('depressed'); - extensionEditor.style.display = 'none'; - } + if (ckeInner) ckeInner.style.removeProperty('display') + if (extensionEditor) extensionEditor.style.display = 'none' +} + +function deactivateBetterEditor(simpleEditorButton: HTMLButtonElement, betterEditorButton: HTMLButtonElement): void { + const ckeInner = document.querySelector('.pane .cke_inner') as HTMLElement; + const ckeContents = document.querySelector('.pane .cke_contents') as HTMLElement; + const extensionEditor = document.querySelector('.extension-editor') as HTMLIFrameElement; + + if (ckeInner) ckeInner.style.removeProperty('display'); + if (ckeContents) ckeContents.style.removeProperty('display'); + if (extensionEditor) extensionEditor.style.removeProperty('display'); + + simpleEditorButton.classList.remove('depressed'); + betterEditorButton.classList.remove('depressed'); } function handleEditorMessage(event: MessageEvent, ckeEditor: HTMLIFrameElement): void { if (!event.origin.includes(browser.runtime.id) || event.data.type !== "message-html") return; - console.log('Message from extension editor', event.data.data); - if (ckeEditor.contentDocument) { ckeEditor.contentDocument.body.innerHTML = event.data.data + ``; }