mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: improve editor + add exporting
This commit is contained in:
@@ -1,11 +0,0 @@
|
|||||||
const METHOD = {
|
|
||||||
GET: 'get',
|
|
||||||
DELETE: 'delete',
|
|
||||||
POST: 'post',
|
|
||||||
PUT: 'put',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
type ObjectValues<T> = T[keyof T]
|
|
||||||
type Method = ObjectValues<typeof METHOD>
|
|
||||||
|
|
||||||
export default Method
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
import { ResponseFn } from './sbFetch'
|
|
||||||
|
|
||||||
export interface ISbStoriesParams
|
|
||||||
extends Partial<ISbStoryData>,
|
|
||||||
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<T extends string> {
|
|
||||||
_uid?: string
|
|
||||||
component?: T
|
|
||||||
_editable?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISbStoryData<
|
|
||||||
Content = ISbComponentType<string> & { [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<IMemoryType | void>
|
|
||||||
set: (key: string, content: ISbResult) => Promise<void>
|
|
||||||
getAll: () => Promise<IMemoryType | void>
|
|
||||||
flush: () => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ISbComponentType<any>>
|
|
||||||
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<string> & { [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<ISbResult>
|
|
||||||
|
|
||||||
export type ArrayFn = (...args: any) => void
|
|
||||||
|
|
||||||
export type HtmlEscapes = {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISbCustomFetch extends Omit<RequestInit, 'method'> {}
|
|
||||||
@@ -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<T extends any[], R> {
|
|
||||||
(...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<any, any>) {
|
|
||||||
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(/<img/g, `<img ${imageAttributes.trim()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof options !== 'boolean' && (options.sizes || options.srcset)) {
|
|
||||||
html = html.replace(/<img.*?src=["|'](.*?)["|']/g, (value: string) => {
|
|
||||||
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(/<img/g, `<img ${renderImageAttributes.trim()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderNode(item: ISbRichtext) {
|
|
||||||
const html = []
|
|
||||||
|
|
||||||
if (item.marks) {
|
|
||||||
item.marks.forEach((m: any) => {
|
|
||||||
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 `</${tags}>`
|
|
||||||
}
|
|
||||||
|
|
||||||
const all = tags
|
|
||||||
.slice(0)
|
|
||||||
.reverse()
|
|
||||||
.map((tag) => {
|
|
||||||
if (tag.constructor === String) {
|
|
||||||
return `</${tag}>`
|
|
||||||
} else {
|
|
||||||
return `</${tag.tag}>`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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<ISbResponse | Error>
|
|
||||||
*/
|
|
||||||
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<ISbResponse | ISbError> {
|
|
||||||
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<ISbResponse | ISbError> {
|
|
||||||
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
|
|
||||||
@@ -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<any> => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ISbComponentType<any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ browser.storage.local.get().then(({ DarkMode }) => {
|
|||||||
|
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.setAttribute("type", "text/css");
|
style.setAttribute("type", "text/css");
|
||||||
|
style.classList.add('iconFamily')
|
||||||
style.innerHTML = `
|
style.innerHTML = `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'IconFamily';
|
font-family: 'IconFamily';
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ body {
|
|||||||
.bn-container > [contenteditable="true"] {
|
.bn-container > [contenteditable="true"] {
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
@@ -2,18 +2,16 @@ import "@blocknote/core/fonts/inter.css";
|
|||||||
import { useCreateBlockNote } from "@blocknote/react";
|
import { useCreateBlockNote } from "@blocknote/react";
|
||||||
import { BlockNoteView } from "@blocknote/mantine";
|
import { BlockNoteView } from "@blocknote/mantine";
|
||||||
import "@blocknote/mantine/style.css";
|
import "@blocknote/mantine/style.css";
|
||||||
//import { generateHTML } from '@tiptap/html'
|
|
||||||
import './Editor.css'
|
import './Editor.css'
|
||||||
|
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const editor = useCreateBlockNote({});
|
const editor = useCreateBlockNote({});
|
||||||
|
|
||||||
/* debounce on change to export to html */
|
|
||||||
editor._tiptapEditor.on('update', () => {
|
editor._tiptapEditor.on('update', () => {
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'message-html',
|
type: 'message-html',
|
||||||
data: editor._tiptapEditor.getHTML()
|
data: editor._tiptapEditor.getHTML(),
|
||||||
}, '*')
|
}, '*')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,41 +6,66 @@ export default async function handleComposeMessage(): Promise<void> {
|
|||||||
console.log('COMPOSE MESSAGE!');
|
console.log('COMPOSE MESSAGE!');
|
||||||
|
|
||||||
const container: HTMLElement | null = document.querySelector('.pane .footer .pillbox');
|
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 buttonHTML = /* html */ `
|
||||||
<button class="button">
|
<button class="button" id="betterEditorButton">
|
||||||
Better Editor
|
Better Editor
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
const button: HTMLElement = stringToHTML(buttonHTML);
|
const button: HTMLElement = stringToHTML(buttonHTML);
|
||||||
|
|
||||||
// Append the new button as the second child of options
|
// Check if the button already exists
|
||||||
firstButton.parentNode?.insertBefore(button.firstChild as Node, firstButton.nextSibling);
|
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
|
// Add click event listeners to the container (event delegation)
|
||||||
container.addEventListener('click', (event: Event) => handleButtonClick(event, container, firstButton));
|
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;
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
if (target && target.classList.contains('button')) {
|
if (target.tagName !== 'BUTTON') return;
|
||||||
const isBetterEditorButton = target.textContent?.trim() === 'Better Editor';
|
|
||||||
|
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) {
|
if (isBetterEditorButton) {
|
||||||
activateBetterEditor(container, firstButton);
|
activateBetterEditor(simpleEditorButton, betterEditorButton);
|
||||||
|
} else if (isSimpleEditorButton) {
|
||||||
|
activateSimpleEditor(simpleEditorButton, betterEditorButton);
|
||||||
} else {
|
} else {
|
||||||
deactivateBetterEditor(container, firstButton);
|
deactivateBetterEditor(simpleEditorButton, betterEditorButton);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.querySelectorAll('button').forEach(btn => btn.classList.remove('depressed'));
|
||||||
|
target.classList.add('depressed');
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateBetterEditor(container: HTMLElement, firstButton: HTMLButtonElement): void {
|
function activateBetterEditor(simpleEditorButton: HTMLButtonElement, betterEditorButton: HTMLButtonElement): void {
|
||||||
firstButton.classList.remove('depressed');
|
// Programmatically click the Simple Editor button first
|
||||||
container.children[1]?.classList.add('depressed');
|
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;
|
const ckeInner = document.querySelector('.pane .cke_inner') as HTMLElement;
|
||||||
if (ckeInner) ckeInner.style.display = 'none';
|
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 });
|
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 ckeInner = document.querySelector('.pane .cke_inner') as HTMLElement;
|
||||||
const extensionEditor = document.querySelector('.extension-editor') as HTMLIFrameElement;
|
const extensionEditor = document.querySelector('.extension-editor') as HTMLIFrameElement;
|
||||||
|
|
||||||
if (ckeInner && extensionEditor) {
|
if (ckeInner) ckeInner.style.removeProperty('display')
|
||||||
ckeInner.style.display = 'block';
|
if (extensionEditor) extensionEditor.style.display = 'none'
|
||||||
firstButton.classList.add('depressed');
|
}
|
||||||
container.children[1]?.classList.remove('depressed');
|
|
||||||
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 {
|
function handleEditorMessage(event: MessageEvent, ckeEditor: HTMLIFrameElement): void {
|
||||||
if (!event.origin.includes(browser.runtime.id) || event.data.type !== "message-html") return;
|
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) {
|
if (ckeEditor.contentDocument) {
|
||||||
ckeEditor.contentDocument.body.innerHTML = event.data.data + `<style>${styles}</style>`;
|
ckeEditor.contentDocument.body.innerHTML = event.data.data + `<style>${styles}</style>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user