@ebl-vue/editor-full 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.postcssrc.yml +33 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +2565 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/postcss.config.js +15 -0
- package/src/components/Editor/Editor.vue +209 -0
- package/src/components/index.ts +27 -0
- package/src/constants/index.ts +1 -0
- package/src/i18n/zh-cn.ts +151 -0
- package/src/icons/index.ts +78 -0
- package/src/index.ts +11 -0
- package/src/installer.ts +22 -0
- package/src/plugins/alert/index.css +150 -0
- package/src/plugins/alert/index.ts +463 -0
- package/src/plugins/block-alignment/index.css +9 -0
- package/src/plugins/block-alignment/index.ts +116 -0
- package/src/plugins/block-alignment/readme.md +1 -0
- package/src/plugins/code/LICENSE +21 -0
- package/src/plugins/code/index.css +120 -0
- package/src/plugins/code/index.ts +530 -0
- package/src/plugins/code/utils/string.ts +34 -0
- package/src/plugins/color-picker/index.ts +138 -0
- package/src/plugins/color-picker/styles.css +27 -0
- package/src/plugins/delimiter/index.css +14 -0
- package/src/plugins/delimiter/index.ts +122 -0
- package/src/plugins/drag-drop/index.css +19 -0
- package/src/plugins/drag-drop/index.ts +151 -0
- package/src/plugins/drag-drop/readme.md +1 -0
- package/src/plugins/header/H1.ts +405 -0
- package/src/plugins/header/H2.ts +403 -0
- package/src/plugins/header/H3.ts +404 -0
- package/src/plugins/header/H4.ts +405 -0
- package/src/plugins/header/H5.ts +405 -0
- package/src/plugins/header/H6.ts +406 -0
- package/src/plugins/header/index.css +20 -0
- package/src/plugins/header/index.ts +15 -0
- package/src/plugins/header/types.d.ts +46 -0
- package/src/plugins/indent/index.css +86 -0
- package/src/plugins/indent/index.ts +697 -0
- package/src/plugins/inline-code/index.css +11 -0
- package/src/plugins/inline-code/index.ts +205 -0
- package/src/plugins/list/ListRenderer/ChecklistRenderer.ts +211 -0
- package/src/plugins/list/ListRenderer/ListRenderer.ts +73 -0
- package/src/plugins/list/ListRenderer/OrderedListRenderer.ts +123 -0
- package/src/plugins/list/ListRenderer/UnorderedListRenderer.ts +123 -0
- package/src/plugins/list/ListRenderer/index.ts +6 -0
- package/src/plugins/list/ListTabulator/index.ts +1179 -0
- package/src/plugins/list/index.ts +502 -0
- package/src/plugins/list/styles/CssPrefix.ts +4 -0
- package/src/plugins/list/styles/icons/index.ts +10 -0
- package/src/plugins/list/styles/input.css +36 -0
- package/src/plugins/list/styles/list.css +165 -0
- package/src/plugins/list/types/Elements.ts +14 -0
- package/src/plugins/list/types/ItemMeta.ts +40 -0
- package/src/plugins/list/types/ListParams.ts +102 -0
- package/src/plugins/list/types/ListRenderer.ts +6 -0
- package/src/plugins/list/types/OlCounterType.ts +63 -0
- package/src/plugins/list/types/index.ts +14 -0
- package/src/plugins/list/utils/focusItem.ts +18 -0
- package/src/plugins/list/utils/getChildItems.ts +40 -0
- package/src/plugins/list/utils/getItemChildWrapper.ts +10 -0
- package/src/plugins/list/utils/getItemContentElement.ts +10 -0
- package/src/plugins/list/utils/getSiblings.ts +52 -0
- package/src/plugins/list/utils/isLastItem.ts +9 -0
- package/src/plugins/list/utils/itemHasSublist.ts +10 -0
- package/src/plugins/list/utils/normalizeData.ts +84 -0
- package/src/plugins/list/utils/removeChildWrapperIfEmpty.ts +31 -0
- package/src/plugins/list/utils/renderToolboxInput.ts +105 -0
- package/src/plugins/list/utils/stripNumbers.ts +7 -0
- package/src/plugins/list/utils/type-guards.ts +8 -0
- package/src/plugins/list.md +15 -0
- package/src/plugins/marker/index.css +4 -0
- package/src/plugins/marker/index.ts +187 -0
- package/src/plugins/paragraph/index.css +23 -0
- package/src/plugins/paragraph/index.ts +380 -0
- package/src/plugins/paragraph/types/icons.d.ts +4 -0
- package/src/plugins/paragraph/utils/makeFragment.ts +17 -0
- package/src/plugins/quote/index.css +26 -0
- package/src/plugins/quote/index.ts +206 -0
- package/src/plugins/table/index.ts +4 -0
- package/src/plugins/table/plugin.ts +254 -0
- package/src/plugins/table/style.css +388 -0
- package/src/plugins/table/table.ts +1192 -0
- package/src/plugins/table/toolbox.ts +165 -0
- package/src/plugins/table/utils/dom.ts +128 -0
- package/src/plugins/table/utils/popover.ts +172 -0
- package/src/plugins/table/utils/throttled.ts +22 -0
- package/src/plugins/underline/index.css +3 -0
- package/src/plugins/underline/index.ts +216 -0
- package/src/plugins/undo/index.ts +509 -0
- package/src/plugins/undo/observer.ts +101 -0
- package/src/style.css +89 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/install.ts +19 -0
- package/tsconfig.json +37 -0
- package/types/index.d.ts +13 -0
- package/types/plugins/index.d.ts +0 -0
- package/vite.config.ts +79 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import type { BlockTune, API, BlockAPI, BlockAddedEvent,ToolConfig } from '@ebl-vue/editorjs/types'
|
|
2
|
+
|
|
3
|
+
import type { MenuConfig } from '@ebl-vue/editorjs/types/tools'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import './index.css'
|
|
7
|
+
|
|
8
|
+
const IconChevronLeft = `<svg t="1763708081701" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2604" width="24" height="24"><path d="M469.3330000000001 725.333H896V640H469.3330000000001v85.333zM128 512l170.667 170.667V341.3330000000001L128 512z m0 384h768v-85.333H128V896z m0-768v85.333h768V128H128z m341.333 256H896v-85.333H469.3330000000001V384z m0 170.667H896v-85.334H469.3330000000001v85.334z" p-id="2605" fill="#000000"></path></svg>`;
|
|
9
|
+
|
|
10
|
+
const IconChevronRight = `<svg t="1763708124227" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2788" width="24" height="24"><path d="M128 896h768v-85.333H128V896z m0-554.667v341.334L298.667 512 128 341.333z m341.333 384H896V640H469.333v85.333zM128 128v85.333h768V128H128z m341.333 256H896v-85.333H469.333V384z m0 170.667H896v-85.334H469.333v85.334z" p-id="2789" fill="#000000"></path></svg>`;
|
|
11
|
+
|
|
12
|
+
interface ConstructorArgs {
|
|
13
|
+
data: IndentData;
|
|
14
|
+
config?: ToolConfig;
|
|
15
|
+
api: API;
|
|
16
|
+
block?: any;
|
|
17
|
+
}
|
|
18
|
+
export type TextDirection = 'ltr' | "rtl"
|
|
19
|
+
|
|
20
|
+
export type IndentTuneConfig = Partial<IndentTuneConfigOptions>
|
|
21
|
+
export type IndentTuneConfigOptions = Record<'indentSize' | 'maxIndent' | 'minIndent', number> & {
|
|
22
|
+
/**
|
|
23
|
+
* Specify the editorjs version so that the styles will match your version
|
|
24
|
+
*/
|
|
25
|
+
version?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Enables auto indent if not null or `true`
|
|
28
|
+
* Default disabled.
|
|
29
|
+
*/
|
|
30
|
+
autoIndent?: {
|
|
31
|
+
/**
|
|
32
|
+
* Tunes you want to apply auto indent for.
|
|
33
|
+
* Defaults to all.
|
|
34
|
+
*/
|
|
35
|
+
tuneNames?: string[]
|
|
36
|
+
} | boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Apply a highlight to the indent if not null
|
|
39
|
+
*/
|
|
40
|
+
highlightIndent?: {
|
|
41
|
+
className?: string,
|
|
42
|
+
/**
|
|
43
|
+
* Tunes you want to apply highlight for.
|
|
44
|
+
* Defaults to all.
|
|
45
|
+
*/
|
|
46
|
+
tuneNames?: string[]
|
|
47
|
+
};
|
|
48
|
+
orientation: 'horizontal' | 'vertical';
|
|
49
|
+
/**
|
|
50
|
+
* Example:
|
|
51
|
+
* {
|
|
52
|
+
* tableTuneName: { min: 2, max:8 },
|
|
53
|
+
* imageTuneName: { min:1 }
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
customBlockIndentLimits: Record<string, Partial<Record<'min' | 'max', number>>>;
|
|
57
|
+
/**
|
|
58
|
+
* Custom keyboard indent handler.
|
|
59
|
+
* Return 'indent' or 'unindent' if you want to change the current indentation.
|
|
60
|
+
* Return 'undefined' or pass 'false' instead of a function to disable the shortcut entirely
|
|
61
|
+
* Return 'default' for default handling
|
|
62
|
+
*/
|
|
63
|
+
handleShortcut?: ((e: KeyboardEvent, blockId: string) => 'indent' | 'unindent' | "default" | undefined) | undefined | false;
|
|
64
|
+
/**
|
|
65
|
+
* `ltr` | `rtl`
|
|
66
|
+
*/
|
|
67
|
+
direction: TextDirection;
|
|
68
|
+
/**
|
|
69
|
+
* Handle dynamic direction change (on each block level)
|
|
70
|
+
*/
|
|
71
|
+
directionChangeHandler: null | ((listener: (blockId: string, direction: TextDirection) => void) => void);
|
|
72
|
+
} & (
|
|
73
|
+
| {
|
|
74
|
+
tuneName: string
|
|
75
|
+
multiblock: true
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
tuneName: null
|
|
79
|
+
multiblock: false
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
const warnings = { orientation: false };
|
|
83
|
+
export type IndentData = { indentLevel: number }
|
|
84
|
+
export default class IndentTune implements BlockTune {
|
|
85
|
+
public static get isTune() {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
public static DATA_WRAPPER_NAME = 'data-block-indent-wrapper'
|
|
89
|
+
public static DATA_FOCUSED = 'data-focused'
|
|
90
|
+
public static DATA_INDENT_LEVEL = "data-indent-level"
|
|
91
|
+
private api: API
|
|
92
|
+
private block: BlockAPI | undefined
|
|
93
|
+
private config: IndentTuneConfigOptions
|
|
94
|
+
public data: IndentData
|
|
95
|
+
private wrapper: HTMLElement = document.createElement('div')
|
|
96
|
+
private DEFAULT_INDENT_KEY = 'Tab' as const;
|
|
97
|
+
constructor({ api, data, config, block, ...other }: ConstructorArgs) {
|
|
98
|
+
this.api = api
|
|
99
|
+
this.block = block
|
|
100
|
+
|
|
101
|
+
const defaultConfig: IndentTuneConfigOptions = {
|
|
102
|
+
indentSize: 24,
|
|
103
|
+
maxIndent: 8,
|
|
104
|
+
minIndent: 0,
|
|
105
|
+
multiblock: false,
|
|
106
|
+
autoIndent: false,
|
|
107
|
+
tuneName: null,
|
|
108
|
+
orientation: 'horizontal',
|
|
109
|
+
customBlockIndentLimits: {},
|
|
110
|
+
handleShortcut: undefined,
|
|
111
|
+
direction: "ltr",
|
|
112
|
+
directionChangeHandler: null,
|
|
113
|
+
version: "2.30",
|
|
114
|
+
}
|
|
115
|
+
if (!config && "settings" in other)
|
|
116
|
+
// for older versions
|
|
117
|
+
config = other.settings as any ?? {}
|
|
118
|
+
this.config = {
|
|
119
|
+
...defaultConfig,
|
|
120
|
+
...(config ?? {}),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.changeConfigBasedOnVersionIfNeeded();
|
|
124
|
+
|
|
125
|
+
if (this.config?.directionChangeHandler) {
|
|
126
|
+
this.config.directionChangeHandler(this.alignmentChangeListener.bind(this));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const defaultIndentLevel = this.config.customBlockIndentLimits[this.block?.name ?? '']?.min ?? this.config.minIndent
|
|
130
|
+
this.data = {
|
|
131
|
+
//@ts-ignore
|
|
132
|
+
indentLevel: defaultIndentLevel,
|
|
133
|
+
...(data ?? {}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.config.multiblock && !this.config.tuneName)
|
|
137
|
+
console.error("IndentTune config 'tuneName' was not provided, this is required for multiblock option to work.")
|
|
138
|
+
|
|
139
|
+
window.addEventListener('resize', (e) => this.onResize.call(this, e))
|
|
140
|
+
|
|
141
|
+
// this is called after the indent tune constructor is created
|
|
142
|
+
this.api.events.on("block changed", ({ event }: { event: BlockAddedEvent }) => {
|
|
143
|
+
const targetId = event.detail.target.id;
|
|
144
|
+
const currentBlockId = this.block?.id;
|
|
145
|
+
const isSameTarget = currentBlockId === targetId
|
|
146
|
+
if (!isSameTarget) return;
|
|
147
|
+
|
|
148
|
+
if (!this.shouldApplyAutoIndent) return
|
|
149
|
+
queueMicrotask(() => this.autoIndentBlock())
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
public prepare?(): void | Promise<void> {
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
public reset?(): void | Promise<void> {
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
public render(): HTMLElement | MenuConfig {
|
|
163
|
+
//Disable items after they are rendered synchronously
|
|
164
|
+
const disableItemOnRender = () => {
|
|
165
|
+
if (this.data.indentLevel == this.maxIndent) {
|
|
166
|
+
const element = this.getTuneButton('indent');
|
|
167
|
+
element?.classList.add(this.CSS.disabledItem)
|
|
168
|
+
if (!element) return true;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
if (this.data.indentLevel == this.minIndent) {
|
|
172
|
+
const element = this.getTuneButton('unindent');
|
|
173
|
+
element?.classList.add(this.CSS.disabledItem)
|
|
174
|
+
if (!element) return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
queueMicrotask(() => {
|
|
178
|
+
const shouldUseMacroTask = disableItemOnRender();
|
|
179
|
+
if (shouldUseMacroTask)
|
|
180
|
+
setTimeout(disableItemOnRender, 300)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if (this.config.orientation === 'vertical') {
|
|
185
|
+
const leftElementName = `${this.TuneNames.indentLeft}-${this.block?.id}`;
|
|
186
|
+
const rightElementName = `${this.TuneNames.indentRight}-${this.block?.id}`
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
title: this.rightText,
|
|
190
|
+
hint: {
|
|
191
|
+
title: this.api.i18n.t(this.rightText),
|
|
192
|
+
},
|
|
193
|
+
onActivate: (item, event) => {
|
|
194
|
+
this.handleIndentRight();
|
|
195
|
+
// override editorjs internal title copy
|
|
196
|
+
//@ts-ignore
|
|
197
|
+
item.title = this.rightText;
|
|
198
|
+
},
|
|
199
|
+
icon: IconChevronRight,
|
|
200
|
+
name: rightElementName,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
title: this.leftText,
|
|
204
|
+
hint: {
|
|
205
|
+
title: this.api.i18n.t(this.leftText),
|
|
206
|
+
},
|
|
207
|
+
onActivate: (item, event) => {
|
|
208
|
+
this.handleIndentLeft()
|
|
209
|
+
//@ts-ignore
|
|
210
|
+
item.title = this.leftText;
|
|
211
|
+
},
|
|
212
|
+
icon: IconChevronLeft,
|
|
213
|
+
name: leftElementName,
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const html = /*html*/ `
|
|
219
|
+
<div class="${this.CSS.popoverItem} ${this.CSS.customPopoverItem}" data-item-name='indent' version=${this.config.version}>
|
|
220
|
+
<button type="button" class="${this.CSS.popoverItemIcon}" data-${this.TuneNames.indentLeft}>${IconChevronLeft}</button>
|
|
221
|
+
<span class="${this.CSS.popoverItemTitle}">${this.api.sanitizer.clean(this.api.i18n.t('Indent'), {})}</span>
|
|
222
|
+
<button type="button" class="${this.CSS.popoverItemIcon}" data-${this.TuneNames.indentRight} style="margin-left:10px;">${IconChevronRight}</button>
|
|
223
|
+
</div>
|
|
224
|
+
`
|
|
225
|
+
|
|
226
|
+
const item = this.createElementFromTemplate(html);
|
|
227
|
+
const rightBtn = item.querySelector(`[data-${this.TuneNames.indentRight}]`);
|
|
228
|
+
const leftBtn = item.querySelector(`[data-${this.TuneNames.indentLeft}]`);
|
|
229
|
+
if (rightBtn) {
|
|
230
|
+
rightBtn.addEventListener('click', () => this.handleIndentRight());
|
|
231
|
+
this.api.tooltip.onHover(rightBtn as HTMLElement, this.api.i18n.t('Indent right'), {
|
|
232
|
+
placement: 'top',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (leftBtn) {
|
|
236
|
+
leftBtn.addEventListener('click', () => this.handleIndentLeft());
|
|
237
|
+
this.api.tooltip.onHover(leftBtn as HTMLElement, this.api.i18n.t('Indent left'), {
|
|
238
|
+
placement: 'top',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return item
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public wrap(pluginsContent: HTMLElement): HTMLElement {
|
|
245
|
+
this.wrapper.appendChild(pluginsContent)
|
|
246
|
+
this.wrapper.setAttribute(IndentTune.DATA_WRAPPER_NAME, '')
|
|
247
|
+
|
|
248
|
+
let applyBlockHighlight = Boolean(this.config.highlightIndent)
|
|
249
|
+
if (this.config.highlightIndent?.tuneNames) {
|
|
250
|
+
const shouldIgnoreThisBlock = !this.config.highlightIndent.tuneNames.includes(this.block?.name ?? "")
|
|
251
|
+
if (shouldIgnoreThisBlock)
|
|
252
|
+
applyBlockHighlight = false
|
|
253
|
+
}
|
|
254
|
+
if (applyBlockHighlight) {
|
|
255
|
+
const highlightEl = this.createElementFromTemplate(/*html*/`
|
|
256
|
+
<div class="${this.config.highlightIndent?.className ?? ""} ${this.CSS.highlightIndent}">
|
|
257
|
+
</div>
|
|
258
|
+
`);
|
|
259
|
+
const contentEl = pluginsContent.classList.contains(this.EditorCSS.content) ? pluginsContent : pluginsContent.querySelector(`.${this.EditorCSS.content}`)
|
|
260
|
+
contentEl?.appendChild(highlightEl);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.applyStylesToWrapper(this.wrapper, this.data.indentLevel)
|
|
264
|
+
|
|
265
|
+
// this.wrapper.addEventListener("keypress", this.handlePropagationForKeyEvent.bind(this), { capture: true })
|
|
266
|
+
// this.wrapper.addEventListener("keyup", this.handlePropagationForKeyEvent.bind(this), { capture: true })
|
|
267
|
+
this.wrapper.addEventListener('keydown', (...args) => this.onKeyDown.apply(this, args), { capture: true })
|
|
268
|
+
this.wrapper.addEventListener("focus", (e) => this.onFocus.call(this, e), { capture: true });
|
|
269
|
+
this.wrapper.addEventListener("blur", (e) => this.onBlur.call(this, e), { capture: true });
|
|
270
|
+
|
|
271
|
+
return this.wrapper
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public save() {
|
|
275
|
+
return this.data
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private get CSS() {
|
|
279
|
+
return {
|
|
280
|
+
customPopoverItem: 'ce-popover-indent-item',
|
|
281
|
+
popoverItem: 'ce-popover-item',
|
|
282
|
+
popoverItemIcon: 'ce-popover-item__icon',
|
|
283
|
+
popoverItemTitle: 'ce-popover-item__title',
|
|
284
|
+
disabledItem: 'ce-popover-item--disabled',
|
|
285
|
+
highlightIndent: "ce-highlight-indent",
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
private get EditorCSS() {
|
|
289
|
+
return {
|
|
290
|
+
block: "ce-block",
|
|
291
|
+
content: "ce-block__content",
|
|
292
|
+
redactor: "codex-editor__redactor",
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private get TuneNames() {
|
|
297
|
+
return {
|
|
298
|
+
indentLeft: 'tune-indent-left',
|
|
299
|
+
indentRight: 'tune-indent-right',
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private get customInterval() {
|
|
304
|
+
return this.config.customBlockIndentLimits[this.block?.name ?? ''] ?? {}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private get maxIndent() {
|
|
308
|
+
return this.customInterval.max ?? this.config.maxIndent
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private get minIndent() {
|
|
312
|
+
return this.customInterval.min ?? this.config.minIndent
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private get isDirectionInverted() {
|
|
316
|
+
return this.config.direction !== 'ltr'; // also ignore invalid directions
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private get rightText() {
|
|
320
|
+
return this.api.i18n.t(this.isDirectionInverted ? 'Un Indent' : 'Indent')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private get leftText() {
|
|
324
|
+
return this.api.i18n.t(this.isDirectionInverted ? 'Indent' : 'Un Indent')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private get shouldApplyAutoIndent(): boolean {
|
|
328
|
+
if (!this.config.autoIndent) return false
|
|
329
|
+
if (typeof this.config.autoIndent === 'boolean') return this.config.autoIndent;
|
|
330
|
+
|
|
331
|
+
// the index is still on the previous block
|
|
332
|
+
const previousBlockIndex = this.api.blocks.getCurrentBlockIndex()
|
|
333
|
+
// const previousBlockIndex = currentBlockIndex// - 1;
|
|
334
|
+
if (previousBlockIndex < 0) return false;
|
|
335
|
+
|
|
336
|
+
const previousBlock = this.api.blocks.getBlockByIndex(previousBlockIndex)
|
|
337
|
+
if (!previousBlock) return false;
|
|
338
|
+
|
|
339
|
+
const previousBlockName = previousBlock.name;
|
|
340
|
+
return !this.config.autoIndent.tuneNames?.length || this.config.autoIndent.tuneNames.includes(previousBlockName)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private handlePropagationForKeyEvent(e: KeyboardEvent): { isIndent: boolean } | null {
|
|
344
|
+
if (!this.block?.id) return null;
|
|
345
|
+
// omit key shortcut entirely
|
|
346
|
+
if (this.config.handleShortcut === false) return null;
|
|
347
|
+
|
|
348
|
+
const isDefaultKeyPressed = e.key == this.DEFAULT_INDENT_KEY
|
|
349
|
+
const isCustomBehaviourDefined = typeof this.config.handleShortcut === 'function'
|
|
350
|
+
|
|
351
|
+
if (!isCustomBehaviourDefined && !isDefaultKeyPressed) return null
|
|
352
|
+
|
|
353
|
+
const handledCommand = this.config.handleShortcut?.(e, this.block.id)
|
|
354
|
+
const shouldIgnoreKeyPress = !handledCommand && isCustomBehaviourDefined
|
|
355
|
+
if (shouldIgnoreKeyPress) return null
|
|
356
|
+
|
|
357
|
+
let isIndent: boolean;
|
|
358
|
+
switch (handledCommand) {
|
|
359
|
+
case 'indent':
|
|
360
|
+
isIndent = true;
|
|
361
|
+
break
|
|
362
|
+
case 'unindent':
|
|
363
|
+
isIndent = false;
|
|
364
|
+
break;
|
|
365
|
+
case 'default':
|
|
366
|
+
default:
|
|
367
|
+
if (!isDefaultKeyPressed) return null;
|
|
368
|
+
isIndent = !e.shiftKey
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
e.stopPropagation()
|
|
372
|
+
e.preventDefault()
|
|
373
|
+
|
|
374
|
+
return { isIndent }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private onKeyDown(e: KeyboardEvent) {
|
|
378
|
+
if (!this.block?.id) return;
|
|
379
|
+
|
|
380
|
+
const handlingResult = this.handlePropagationForKeyEvent(e)
|
|
381
|
+
if (!handlingResult) return;
|
|
382
|
+
const { isIndent } = handlingResult
|
|
383
|
+
|
|
384
|
+
//this might be still open
|
|
385
|
+
this.api.inlineToolbar.close()
|
|
386
|
+
const selectedBlocks = this.getGlobalSelectedBlocks()
|
|
387
|
+
const isSingleLineBlock = !this.config.multiblock || selectedBlocks.length < 2
|
|
388
|
+
if (isSingleLineBlock) {
|
|
389
|
+
if (isIndent) this.indentBlock()
|
|
390
|
+
else this.unIndentBlock()
|
|
391
|
+
this.block.dispatchChange?.()
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!Boolean(this.config.tuneName)) {
|
|
396
|
+
console.error(`'tuneName' is empty.`)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
selectedBlocks.forEach(async (b) => {
|
|
401
|
+
//get block indent level
|
|
402
|
+
const savedData = await b.save()
|
|
403
|
+
if (!savedData) return
|
|
404
|
+
|
|
405
|
+
if (!('tunes' in savedData) || typeof savedData.tunes !== 'object' || !savedData.tunes) {
|
|
406
|
+
console.error('Multiblock indenting is not supported for this editor version. ')
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
//this somehow SAVES fine
|
|
411
|
+
const tune = (savedData.tunes as Record<string, IndentData>)[this.config.tuneName ?? ''] as IndentData | undefined
|
|
412
|
+
if (!tune) {
|
|
413
|
+
console.error(`'tuneName' is invalid, no tune was found for block ${b.name}`)
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
if (isIndent) tune.indentLevel = Math.min(this.config.maxIndent, (tune.indentLevel ?? 0) + 1)
|
|
417
|
+
else tune.indentLevel = Math.max(0, (tune.indentLevel ?? 0) - 1)
|
|
418
|
+
b.dispatchChange?.()
|
|
419
|
+
|
|
420
|
+
//apply visual feedback manually, since we can't make the tune update on other blocks
|
|
421
|
+
const blockWrapper = this.getWrapperBlockById(b.id)
|
|
422
|
+
if (blockWrapper instanceof HTMLElement) {
|
|
423
|
+
this.applyStylesToWrapper(blockWrapper, tune.indentLevel)
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private handleIndentLeft() {
|
|
429
|
+
if (this.isDirectionInverted)
|
|
430
|
+
this.indentBlock();
|
|
431
|
+
else
|
|
432
|
+
this.unIndentBlock();
|
|
433
|
+
this.block?.dispatchChange?.()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private handleIndentRight() {
|
|
437
|
+
if (this.isDirectionInverted)
|
|
438
|
+
this.unIndentBlock();
|
|
439
|
+
else
|
|
440
|
+
this.indentBlock();
|
|
441
|
+
this.block?.dispatchChange?.()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private indentBlock() {
|
|
445
|
+
if (!this.wrapper) return
|
|
446
|
+
this.data.indentLevel = Math.min(this.data.indentLevel + 1, this.maxIndent)
|
|
447
|
+
|
|
448
|
+
this.applyStylesToWrapper(this.wrapper, this.data.indentLevel)
|
|
449
|
+
|
|
450
|
+
this.toggleDisableStateForButtons()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private unIndentBlock() {
|
|
454
|
+
if (!this.wrapper) return
|
|
455
|
+
this.data.indentLevel = Math.max(this.data.indentLevel - 1, this.minIndent)
|
|
456
|
+
|
|
457
|
+
this.applyStylesToWrapper(this.wrapper, this.data.indentLevel)
|
|
458
|
+
|
|
459
|
+
this.toggleDisableStateForButtons()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private autoIndentBlock() {
|
|
463
|
+
const currentBlockIndex = this.api.blocks.getBlockIndex(this.block!.id)
|
|
464
|
+
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1)
|
|
465
|
+
|
|
466
|
+
if (!previousBlock) return
|
|
467
|
+
|
|
468
|
+
const previousBlockIndentLevelAttribute = previousBlock.holder?.querySelector(`[${IndentTune.DATA_WRAPPER_NAME}]`)
|
|
469
|
+
?.getAttribute(IndentTune.DATA_INDENT_LEVEL);
|
|
470
|
+
|
|
471
|
+
const previousBlockIndentLevel = Number(
|
|
472
|
+
previousBlockIndentLevelAttribute ?? 0,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
const currentBlockIndentLevel = Math.min(Math.max(previousBlockIndentLevel, this.minIndent), this.maxIndent)
|
|
476
|
+
|
|
477
|
+
this.data.indentLevel = currentBlockIndentLevel
|
|
478
|
+
|
|
479
|
+
this.applyStylesToWrapper(this.wrapper, this.data.indentLevel)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private toggleDisableStateForButtons() {
|
|
483
|
+
if (this.data.indentLevel === this.minIndent)
|
|
484
|
+
this.getTuneButton('unindent')?.classList.add(this.CSS.disabledItem)
|
|
485
|
+
else
|
|
486
|
+
this.getTuneButton('unindent')?.classList.remove(this.CSS.disabledItem)
|
|
487
|
+
|
|
488
|
+
if (this.data.indentLevel === this.maxIndent)
|
|
489
|
+
this.getTuneButton('indent')?.classList.add(this.CSS.disabledItem)
|
|
490
|
+
else
|
|
491
|
+
this.getTuneButton('indent')?.classList.remove(this.CSS.disabledItem)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private getTuneButton(indentType: 'indent' | 'unindent') {
|
|
495
|
+
let indentName: 'indentLeft' | "indentRight" = indentType === 'indent' ? "indentRight" : "indentLeft";
|
|
496
|
+
if (this.isDirectionInverted)
|
|
497
|
+
indentName = indentType == 'indent' ? "indentLeft" : "indentRight";
|
|
498
|
+
|
|
499
|
+
return this.config.orientation === 'vertical'
|
|
500
|
+
? this.getTuneByName(`${this.TuneNames[indentName]}-${this.block?.id}`)
|
|
501
|
+
: document.querySelector(`.${this.CSS.popoverItemIcon}[data-${this.TuneNames[indentName]}]`)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private getTuneByName(name: string) {
|
|
505
|
+
return document.querySelector(`.${this.CSS.popoverItem}[data-item-name="${name}"]`)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private getTuneTitleByName(name: string) {
|
|
509
|
+
return this.getTuneByName(name)?.querySelector(`.${this.CSS.popoverItemTitle}`)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private applyStylesToWrapper(givenWrapper: HTMLElement, indentLevel: number = parseInt(givenWrapper.getAttribute(IndentTune.DATA_INDENT_LEVEL) || "0")) {
|
|
513
|
+
const indentValue = indentLevel * this.config.indentSize;
|
|
514
|
+
givenWrapper.setAttribute(IndentTune.DATA_INDENT_LEVEL, indentLevel.toString());
|
|
515
|
+
|
|
516
|
+
const contentElement = givenWrapper.querySelector(`.${this.EditorCSS.content}`);
|
|
517
|
+
const blockElement = this.getBlockForWrapper(givenWrapper) || document.querySelector(`.${this.EditorCSS.redactor}`);
|
|
518
|
+
if (!(contentElement instanceof HTMLElement) || !blockElement) return;
|
|
519
|
+
|
|
520
|
+
const blockWidth = blockElement.getBoundingClientRect().width;
|
|
521
|
+
if (blockWidth === 0) //block is not in DOM yet/redactor is hidden, depends on editorjs version
|
|
522
|
+
{
|
|
523
|
+
queueMicrotask(() => this.applyStylesToWrapper.bind(this)(givenWrapper, indentLevel))
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
const normalContentWidth = this.maxWidthForContent(givenWrapper);
|
|
527
|
+
|
|
528
|
+
// until margin inline == 0;
|
|
529
|
+
const maxApplyableIndent = (blockWidth - normalContentWidth) / 2
|
|
530
|
+
|
|
531
|
+
const indentToApply = Math.max(0, Math.min(maxApplyableIndent, indentValue));
|
|
532
|
+
//have to double the value because content inside has margin inline;
|
|
533
|
+
const indentValuePixels = `${indentToApply * 2}px`;
|
|
534
|
+
const indentValuePixelsForHighlight = `${indentToApply}px`;
|
|
535
|
+
|
|
536
|
+
// because the direction has been changed
|
|
537
|
+
// const omitTransitionTemporarily = givenWrapper.style[this.isDirectionInverted ? 'paddingLeft' : "paddingRight"] === "0px"
|
|
538
|
+
// if (omitTransitionTemporarily) this.omitTransitionTemporarily(givenWrapper)
|
|
539
|
+
|
|
540
|
+
if (this.isDirectionInverted) {
|
|
541
|
+
givenWrapper.style.paddingLeft = '0px';
|
|
542
|
+
givenWrapper.style.paddingRight = indentValuePixels;
|
|
543
|
+
} else {
|
|
544
|
+
givenWrapper.style.paddingLeft = indentValuePixels;
|
|
545
|
+
givenWrapper.style.paddingRight = "0px";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const highlightElement = givenWrapper.querySelector(`.${this.CSS.highlightIndent}`)
|
|
549
|
+
if (!(highlightElement instanceof HTMLElement)) return;
|
|
550
|
+
|
|
551
|
+
// if (omitTransitionTemporarily) this.omitTransitionTemporarily(highlightElement)
|
|
552
|
+
|
|
553
|
+
if (this.isDirectionInverted) {
|
|
554
|
+
highlightElement.style.width = indentValuePixelsForHighlight;
|
|
555
|
+
highlightElement.style.left = "100%";
|
|
556
|
+
highlightElement.style.right = '';
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
highlightElement.style.width = indentValuePixelsForHighlight;
|
|
560
|
+
highlightElement.style.left = "";
|
|
561
|
+
highlightElement.style.right = '100%';
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private onFocus(e: FocusEvent) {
|
|
566
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
567
|
+
const isInsideCurrentBlock = this.wrapper.contains(e.target);
|
|
568
|
+
if (!isInsideCurrentBlock) return;
|
|
569
|
+
this.wrapper.setAttribute(IndentTune.DATA_FOCUSED, '');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private onBlur(e: FocusEvent) {
|
|
573
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
574
|
+
const isInsideCurrentBlock = this.wrapper.contains(e.target);
|
|
575
|
+
if (!isInsideCurrentBlock) return;
|
|
576
|
+
this.wrapper.removeAttribute(IndentTune.DATA_FOCUSED);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private lastResizeTimeout: null | NodeJS.Timeout = null;
|
|
580
|
+
private onResize(e: UIEvent) {
|
|
581
|
+
const timeoutDelayMs = 500;
|
|
582
|
+
if (this.lastResizeTimeout)
|
|
583
|
+
clearTimeout(this.lastResizeTimeout)
|
|
584
|
+
this.lastResizeTimeout = setTimeout(() => {
|
|
585
|
+
const allWrappers = document.querySelectorAll(`[${IndentTune.DATA_WRAPPER_NAME}]`);
|
|
586
|
+
allWrappers.forEach((w) => {
|
|
587
|
+
if (!(w instanceof HTMLElement)) return;
|
|
588
|
+
this.applyStylesToWrapper(w);
|
|
589
|
+
});
|
|
590
|
+
}, timeoutDelayMs);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private getGlobalSelectedBlocks() {
|
|
594
|
+
const crossSelectedBlocks = new Array(this.api.blocks.getBlocksCount())
|
|
595
|
+
.fill(0)
|
|
596
|
+
.map((_, idx) => this.api.blocks.getBlockByIndex(idx))
|
|
597
|
+
.filter((b): b is BlockAPI => !!b?.selected)
|
|
598
|
+
return crossSelectedBlocks
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private getWrapperBlockById(blockId: string) {
|
|
602
|
+
const selector = `.${this.EditorCSS.block}[data-id="${blockId}"] [${IndentTune.DATA_WRAPPER_NAME}]`
|
|
603
|
+
return document.querySelector(selector) ??
|
|
604
|
+
this.api.blocks.getById(blockId)?.holder.querySelector(`[${IndentTune.DATA_WRAPPER_NAME}]`)
|
|
605
|
+
?? null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private getBlockForWrapper(wrapper: HTMLElement): HTMLElement | null {
|
|
609
|
+
let current = wrapper;
|
|
610
|
+
while ((!current.classList.contains(this.EditorCSS.block))) {
|
|
611
|
+
if (!current.parentElement || (current instanceof HTMLHtmlElement)) return null;
|
|
612
|
+
current = current.parentElement;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return current
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private alignmentChangeListener(blockId: string, direction: TextDirection) {
|
|
619
|
+
// across all blocks this function is called, so we got to filter out
|
|
620
|
+
if (blockId !== this.block?.id) return;
|
|
621
|
+
const hasDirectionChanged = direction !== this.config.direction
|
|
622
|
+
if (!hasDirectionChanged) return
|
|
623
|
+
|
|
624
|
+
this.config.direction = direction;
|
|
625
|
+
this.applyStylesToWrapper(this.wrapper, this.data.indentLevel)
|
|
626
|
+
this.toggleDisableStateForButtons()
|
|
627
|
+
if (this.config.orientation === 'vertical') {
|
|
628
|
+
// I have to update the text for the indent options 😪
|
|
629
|
+
|
|
630
|
+
const indentRightBtnTitle = this.getTuneTitleByName(`${this.TuneNames.indentRight}-${this.block?.id}`);
|
|
631
|
+
if (indentRightBtnTitle) indentRightBtnTitle.textContent = this.rightText
|
|
632
|
+
|
|
633
|
+
const indentLeftBtnTitle = this.getTuneTitleByName(`${this.TuneNames.indentLeft}-${this.block?.id}`);
|
|
634
|
+
if (indentLeftBtnTitle) indentLeftBtnTitle.textContent = this.leftText
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private createElementFromTemplate(template: string): HTMLElement {
|
|
639
|
+
return new DOMParser().parseFromString(template, 'text/html').body.firstChild as HTMLElement;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// private omitTransitionTemporarily(element: HTMLElement) {
|
|
643
|
+
// element.style.transitionDuration = "0s";
|
|
644
|
+
// (() => {
|
|
645
|
+
// element.style.transitionDuration = "";
|
|
646
|
+
// })
|
|
647
|
+
// }
|
|
648
|
+
|
|
649
|
+
private changeConfigBasedOnVersionIfNeeded() {
|
|
650
|
+
if (!this.config.version) return;
|
|
651
|
+
|
|
652
|
+
if (this.config.version < '2.27' && this.config.orientation === 'vertical') {
|
|
653
|
+
this.config.orientation = 'horizontal';
|
|
654
|
+
|
|
655
|
+
if (!warnings.orientation)
|
|
656
|
+
console.warn("Current editor version does not support vertical indent tune 'orientation'. View your config input")
|
|
657
|
+
warnings.orientation = true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private cachedMaxWidthForContent: number | null = null;
|
|
662
|
+
private maxWidthForContent(elementInsideEditor: HTMLElement): number {
|
|
663
|
+
const content = elementInsideEditor.querySelector(`.${this.EditorCSS.content}`);
|
|
664
|
+
if ((content instanceof HTMLElement)) {
|
|
665
|
+
const { maxWidth } = window.getComputedStyle(content);
|
|
666
|
+
if (maxWidth) {
|
|
667
|
+
this.cachedMaxWidthForContent = parseInt(maxWidth);
|
|
668
|
+
return this.cachedMaxWidthForContent
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (this.cachedMaxWidthForContent !== null) return this.cachedMaxWidthForContent
|
|
673
|
+
// Get value from stylesheet
|
|
674
|
+
// for (let i = 0; i < document.styleSheets.length; i++) {
|
|
675
|
+
// const styleSheet = document.styleSheets.item(i);
|
|
676
|
+
// if (!styleSheet || !(styleSheet.ownerNode instanceof HTMLStyleElement) || styleSheet.ownerNode.id !== "editor-js-styles") continue;
|
|
677
|
+
|
|
678
|
+
// for (let j = 0; j < styleSheet.cssRules.length; j++) {
|
|
679
|
+
// const rule = styleSheet.cssRules.item(j);
|
|
680
|
+
// if (!rule) continue;
|
|
681
|
+
// const selector = `.${this.EditorCSS.content}`
|
|
682
|
+
// if (!rule.cssText.startsWith(selector + " {") && (rule as { selectorText?: string }).selectorText !== selector)
|
|
683
|
+
// continue;
|
|
684
|
+
// const matches = /max-width: [\d]+px;/.exec(rule.cssText)
|
|
685
|
+
// if (!matches || !matches.length) continue;
|
|
686
|
+
|
|
687
|
+
// const maxWidth = parseInt(matches[0].replace("max-width:", ''));
|
|
688
|
+
// this.cachedMaxWidthForContent = maxWidth;
|
|
689
|
+
// return this.maxWidthForContent;
|
|
690
|
+
// }
|
|
691
|
+
|
|
692
|
+
// }
|
|
693
|
+
// console.warn("Cannot detect EditorJs max width for content. Please contact package author")
|
|
694
|
+
this.cachedMaxWidthForContent = 650;
|
|
695
|
+
return this.cachedMaxWidthForContent;
|
|
696
|
+
}
|
|
697
|
+
}
|