mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: add experimental better editor
This commit is contained in:
@@ -35,6 +35,9 @@
|
||||
"url": "^0.11.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "^0.14.1",
|
||||
"@blocknote/mantine": "^0.14.1",
|
||||
"@blocknote/react": "^0.14.1",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@million/lint": "latest",
|
||||
|
||||
+6
-3
@@ -27,6 +27,7 @@ import { initializeSettingsState, settingsState } from './seqta/utils/listeners/
|
||||
import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges'
|
||||
import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements'
|
||||
import { eventManager } from './seqta/utils/listeners/EventManager'
|
||||
import handleComposeMessage from './seqta/ui/customMessageEditor'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -65,9 +66,6 @@ async function init() {
|
||||
|
||||
if (settingsState.onoff) {
|
||||
enableCurrentTheme()
|
||||
//console.log(await browser.storage.local.get())
|
||||
//settingsState.bksliderinput = '10'
|
||||
//console.log(await browser.storage.local.get())
|
||||
|
||||
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
@@ -563,6 +561,11 @@ async function LoadPageElements(): Promise<void> {
|
||||
className: 'timetablepage',
|
||||
}, handleTimetable);
|
||||
|
||||
eventManager.register('composeMessage', {
|
||||
elementType: 'div',
|
||||
customCheck: (element: Element) => element.querySelector('.coneqtMessage') !== null
|
||||
}, handleComposeMessage);
|
||||
|
||||
await handleSublink(sublink);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,14 @@ html {
|
||||
background-color 200ms ease-in-out,
|
||||
backdrop-filter 200ms ease-in-out;
|
||||
}
|
||||
.extension-editor {
|
||||
background: var(--background-primary);
|
||||
border-radius: 16px;
|
||||
border: none !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: visible !important;
|
||||
}
|
||||
#themeCreatorIframe {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -0,0 +1,329 @@
|
||||
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'> {}
|
||||
@@ -0,0 +1,396 @@
|
||||
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
|
||||
@@ -0,0 +1,192 @@
|
||||
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
|
||||
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import font from '../resources/fonts/IconFamily.woff'
|
||||
|
||||
import ThemeCreator from './pages/ThemeCreator';
|
||||
import Store from './pages/Store';
|
||||
import Editor from './pages/Editor';
|
||||
|
||||
browser.storage.local.get().then(({ DarkMode }) => {
|
||||
if (DarkMode) document.documentElement.classList.add('dark');
|
||||
@@ -51,6 +52,7 @@ root.render(
|
||||
<Route path="/settings/embedded" element={<SettingsPage standalone={false} />} />
|
||||
<Route path="/store" element={<Store />} />
|
||||
<Route path="/themeCreator" element={<ThemeCreator />} />
|
||||
<Route path="/editor" element={<Editor />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
body {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.bn-container[data-color-scheme=dark] {
|
||||
--bn-colors-editor-background: #18181B00 !important;
|
||||
}
|
||||
|
||||
.bn-container > [contenteditable="true"] {
|
||||
height: 100vh !important;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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()
|
||||
}, '*')
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="h-screen">
|
||||
<BlockNoteView editor={editor} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -231,10 +231,10 @@ function GetLightDarkModeString(darkMode: boolean) {
|
||||
async function addDarkLightToggle() {
|
||||
const tooltipString = GetLightDarkModeString(settingsState.DarkMode);
|
||||
const svgContent = settingsState.DarkMode ?
|
||||
'<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>' :
|
||||
'<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>';
|
||||
/* html */`<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>` :
|
||||
/* html */`<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
||||
|
||||
const LightDarkModeButton = stringToHTML(`
|
||||
const LightDarkModeButton = stringToHTML(/* html */`
|
||||
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">${svgContent}</svg>
|
||||
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import stringToHTML from "../utils/stringToHTML";
|
||||
import browser from 'webextension-polyfill';
|
||||
import styles from "@blocknote/mantine/style.css?raw";
|
||||
|
||||
export default async function handleComposeMessage(): Promise<void> {
|
||||
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;
|
||||
|
||||
if (container && firstButton) {
|
||||
const buttonHTML = /* html */ `
|
||||
<button class="button">
|
||||
Better Editor
|
||||
</button>
|
||||
`;
|
||||
const button: HTMLElement = stringToHTML(buttonHTML);
|
||||
|
||||
// Append the new button as the second child of options
|
||||
firstButton.parentNode?.insertBefore(button.firstChild as Node, firstButton.nextSibling);
|
||||
|
||||
// Add click event listeners to both buttons
|
||||
container.addEventListener('click', (event: Event) => handleButtonClick(event, container, firstButton));
|
||||
}
|
||||
}
|
||||
|
||||
function handleButtonClick(event: Event, container: HTMLElement, firstButton: HTMLButtonElement): void {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (target && target.classList.contains('button')) {
|
||||
const isBetterEditorButton = target.textContent?.trim() === 'Better Editor';
|
||||
|
||||
if (isBetterEditorButton) {
|
||||
activateBetterEditor(container, firstButton);
|
||||
} else {
|
||||
deactivateBetterEditor(container, firstButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateBetterEditor(container: HTMLElement, firstButton: HTMLButtonElement): void {
|
||||
firstButton.classList.remove('depressed');
|
||||
container.children[1]?.classList.add('depressed');
|
||||
|
||||
const ckeInner = document.querySelector('.pane .cke_inner') as HTMLElement;
|
||||
if (ckeInner) ckeInner.style.display = 'none';
|
||||
|
||||
let extensionEditor: HTMLIFrameElement | null = document.querySelector('.extension-editor') as HTMLIFrameElement;
|
||||
if (extensionEditor) {
|
||||
extensionEditor.style.display = 'block';
|
||||
} else {
|
||||
const extensionEditorIframe: HTMLIFrameElement = document.createElement('iframe');
|
||||
extensionEditorIframe.src = `${browser.runtime.getURL('src/interface/index.html')}#editor`;
|
||||
extensionEditorIframe.setAttribute('allowTransparency', 'true');
|
||||
extensionEditorIframe.setAttribute('excludeDarkCheck', 'true');
|
||||
extensionEditorIframe.classList.add('extension-editor');
|
||||
document.getElementById('cke_editor1')?.appendChild(extensionEditorIframe);
|
||||
}
|
||||
|
||||
extensionEditor = document.querySelector('.extension-editor') as HTMLIFrameElement;
|
||||
const ckeEditor = document.querySelector('#cke_1_contents iframe.cke_wysiwyg_frame') as HTMLIFrameElement;
|
||||
|
||||
window.addEventListener('message', (event) => handleEditorMessage(event, ckeEditor), { once: true });
|
||||
}
|
||||
|
||||
function deactivateBetterEditor(container: HTMLElement, firstButton: HTMLButtonElement): void {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
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 + `<style>${styles}</style>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user