@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.
Files changed (101) hide show
  1. package/.postcssrc.yml +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.mjs +2565 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +55 -0
  8. package/postcss.config.js +15 -0
  9. package/src/components/Editor/Editor.vue +209 -0
  10. package/src/components/index.ts +27 -0
  11. package/src/constants/index.ts +1 -0
  12. package/src/i18n/zh-cn.ts +151 -0
  13. package/src/icons/index.ts +78 -0
  14. package/src/index.ts +11 -0
  15. package/src/installer.ts +22 -0
  16. package/src/plugins/alert/index.css +150 -0
  17. package/src/plugins/alert/index.ts +463 -0
  18. package/src/plugins/block-alignment/index.css +9 -0
  19. package/src/plugins/block-alignment/index.ts +116 -0
  20. package/src/plugins/block-alignment/readme.md +1 -0
  21. package/src/plugins/code/LICENSE +21 -0
  22. package/src/plugins/code/index.css +120 -0
  23. package/src/plugins/code/index.ts +530 -0
  24. package/src/plugins/code/utils/string.ts +34 -0
  25. package/src/plugins/color-picker/index.ts +138 -0
  26. package/src/plugins/color-picker/styles.css +27 -0
  27. package/src/plugins/delimiter/index.css +14 -0
  28. package/src/plugins/delimiter/index.ts +122 -0
  29. package/src/plugins/drag-drop/index.css +19 -0
  30. package/src/plugins/drag-drop/index.ts +151 -0
  31. package/src/plugins/drag-drop/readme.md +1 -0
  32. package/src/plugins/header/H1.ts +405 -0
  33. package/src/plugins/header/H2.ts +403 -0
  34. package/src/plugins/header/H3.ts +404 -0
  35. package/src/plugins/header/H4.ts +405 -0
  36. package/src/plugins/header/H5.ts +405 -0
  37. package/src/plugins/header/H6.ts +406 -0
  38. package/src/plugins/header/index.css +20 -0
  39. package/src/plugins/header/index.ts +15 -0
  40. package/src/plugins/header/types.d.ts +46 -0
  41. package/src/plugins/indent/index.css +86 -0
  42. package/src/plugins/indent/index.ts +697 -0
  43. package/src/plugins/inline-code/index.css +11 -0
  44. package/src/plugins/inline-code/index.ts +205 -0
  45. package/src/plugins/list/ListRenderer/ChecklistRenderer.ts +211 -0
  46. package/src/plugins/list/ListRenderer/ListRenderer.ts +73 -0
  47. package/src/plugins/list/ListRenderer/OrderedListRenderer.ts +123 -0
  48. package/src/plugins/list/ListRenderer/UnorderedListRenderer.ts +123 -0
  49. package/src/plugins/list/ListRenderer/index.ts +6 -0
  50. package/src/plugins/list/ListTabulator/index.ts +1179 -0
  51. package/src/plugins/list/index.ts +502 -0
  52. package/src/plugins/list/styles/CssPrefix.ts +4 -0
  53. package/src/plugins/list/styles/icons/index.ts +10 -0
  54. package/src/plugins/list/styles/input.css +36 -0
  55. package/src/plugins/list/styles/list.css +165 -0
  56. package/src/plugins/list/types/Elements.ts +14 -0
  57. package/src/plugins/list/types/ItemMeta.ts +40 -0
  58. package/src/plugins/list/types/ListParams.ts +102 -0
  59. package/src/plugins/list/types/ListRenderer.ts +6 -0
  60. package/src/plugins/list/types/OlCounterType.ts +63 -0
  61. package/src/plugins/list/types/index.ts +14 -0
  62. package/src/plugins/list/utils/focusItem.ts +18 -0
  63. package/src/plugins/list/utils/getChildItems.ts +40 -0
  64. package/src/plugins/list/utils/getItemChildWrapper.ts +10 -0
  65. package/src/plugins/list/utils/getItemContentElement.ts +10 -0
  66. package/src/plugins/list/utils/getSiblings.ts +52 -0
  67. package/src/plugins/list/utils/isLastItem.ts +9 -0
  68. package/src/plugins/list/utils/itemHasSublist.ts +10 -0
  69. package/src/plugins/list/utils/normalizeData.ts +84 -0
  70. package/src/plugins/list/utils/removeChildWrapperIfEmpty.ts +31 -0
  71. package/src/plugins/list/utils/renderToolboxInput.ts +105 -0
  72. package/src/plugins/list/utils/stripNumbers.ts +7 -0
  73. package/src/plugins/list/utils/type-guards.ts +8 -0
  74. package/src/plugins/list.md +15 -0
  75. package/src/plugins/marker/index.css +4 -0
  76. package/src/plugins/marker/index.ts +187 -0
  77. package/src/plugins/paragraph/index.css +23 -0
  78. package/src/plugins/paragraph/index.ts +380 -0
  79. package/src/plugins/paragraph/types/icons.d.ts +4 -0
  80. package/src/plugins/paragraph/utils/makeFragment.ts +17 -0
  81. package/src/plugins/quote/index.css +26 -0
  82. package/src/plugins/quote/index.ts +206 -0
  83. package/src/plugins/table/index.ts +4 -0
  84. package/src/plugins/table/plugin.ts +254 -0
  85. package/src/plugins/table/style.css +388 -0
  86. package/src/plugins/table/table.ts +1192 -0
  87. package/src/plugins/table/toolbox.ts +165 -0
  88. package/src/plugins/table/utils/dom.ts +128 -0
  89. package/src/plugins/table/utils/popover.ts +172 -0
  90. package/src/plugins/table/utils/throttled.ts +22 -0
  91. package/src/plugins/underline/index.css +3 -0
  92. package/src/plugins/underline/index.ts +216 -0
  93. package/src/plugins/undo/index.ts +509 -0
  94. package/src/plugins/undo/observer.ts +101 -0
  95. package/src/style.css +89 -0
  96. package/src/utils/index.ts +15 -0
  97. package/src/utils/install.ts +19 -0
  98. package/tsconfig.json +37 -0
  99. package/types/index.d.ts +13 -0
  100. package/types/plugins/index.d.ts +0 -0
  101. 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
+ }