@alitons/ckeditor5 0.0.30 → 0.0.31

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.
@@ -0,0 +1,7 @@
1
+ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
2
+ import Widget from '@ckeditor/ckeditor5-widget/src/widget';
3
+ export default class DocumentosPlugin extends Plugin {
4
+ static get pluginName(): string;
5
+ static get requires(): (typeof Widget)[];
6
+ init(): Promise<void>;
7
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@alitons/ckeditor5",
3
3
  "author": "Aliton Silva",
4
4
  "description": "Ckeditor 5 Personalizado adicionados campos para atender as demandas da SEAD/AC",
5
- "version": "0.0.30",
5
+ "version": "0.0.31",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
7
  "private": false,
8
8
  "main": "./build/ckeditor.js",
package/sample/script.js CHANGED
@@ -115,7 +115,9 @@ watchdog.create( document.querySelector( '.editor' ), {
115
115
  'importarsei',
116
116
  'documentoModelo',
117
117
  '|',
118
- 'conteudoDinamico:produtos'
118
+ 'conteudoDinamico:produtos',
119
+ '|',
120
+ 'insertDocumento'
119
121
  ]
120
122
  },
121
123
  heading: {
@@ -132,10 +134,50 @@ watchdog.create( document.querySelector( '.editor' ), {
132
134
  uiClass: 'ui-fancy' // isso é pra UI (dropdown)
133
135
  }
134
136
  ]
135
- }
137
+ },
138
+ inserirDocumento: {
139
+ dropdownLabel: 'Inserir Documento',
140
+ getItems: async () => {
141
+ const lista = await buscarLista();
142
+
143
+ return lista.map(item => ({
144
+ id: item.id,
145
+ label: item.nome,
146
+ showLabel: false,
147
+ getHtml: async () => {
148
+ const result = await buscarConteudo(item.id);
149
+ return result.html;
150
+ }
151
+ }));
152
+ }
153
+ }
136
154
 
137
155
  })
138
156
 
157
+ async function buscarLista() {
158
+ // Simula uma chamada AJAX para buscar a lista de documentos
159
+ return new Promise(resolve => {
160
+ setTimeout(() => {
161
+ resolve([
162
+ { id: 1, nome: 'Documento A' },
163
+ { id: 2, nome: 'Documento B' },
164
+ { id: 3, nome: 'Documento C' }
165
+ ]);
166
+ }, 1500);
167
+ });
168
+ }
169
+
170
+ async function buscarConteudo(id) {
171
+ // Simula uma chamada AJAX para buscar o conteúdo do documento selecionado
172
+ return new Promise(resolve => {
173
+ setTimeout(() => {
174
+ resolve({
175
+ html: `<p>Conteúdo do Documento ${id}</p>`
176
+ });
177
+ }, 1500);
178
+ });
179
+ }
180
+
139
181
  let prevState = watchdog.state;
140
182
 
141
183
  watchdog.on( 'stateChange', () => {
@@ -166,11 +208,24 @@ document.getElementById('get-html').addEventListener('click', () => {
166
208
  document.getElementById('update-content').addEventListener('click', () => {
167
209
  const editor = watchdog.editor;
168
210
  const conteudoDinamico = editor.plugins.get('ConteudoDinamicoPlugin');
169
- conteudoDinamico.updateBySourceId('tabela:produtos', `<table border="1" cellpadding="5" cellspacing="0">
211
+ conteudoDinamico.updateBySourceId('tabela:produtos', `<table><thead>
170
212
  <tr>
171
213
  <th>Produto</th>
172
214
  <th>Preço</th>
173
215
  <th>Quantidade</th>
174
216
  </tr>
217
+ </thead>
218
+ <tbody>
219
+ <tr>
220
+ <td>Caneta</td>
221
+ <td>R$ 2,50</td>
222
+ <td>${Math.floor(Math.random() * 100)}</td>
223
+ </tr>
224
+ <tr>
225
+ <td>Caderno</td>
226
+ <td>R$ 15,00</td>
227
+ <td>${Math.floor(Math.random() * 100)}</td>
228
+ </tr>
229
+ </tbody>
175
230
  </table>`);
176
231
  });
package/src/ckeditor.ts CHANGED
@@ -91,6 +91,7 @@ import HeadingUiClassPlugin from './plugins/HeadingUiClassPlugin';
91
91
  // import NumberedDivList from './plugins/listaNumerada';
92
92
  // import NumberedDivListSplit from './plugins/NumberedDivListSplit';
93
93
  import ConteudoDinamicoPlugin from './plugins/ConteudoDinamicoPlugin';
94
+ import DocumentosPlugin from './plugins/DocumentosPlugin';
94
95
 
95
96
  import './css/custom.css';
96
97
  import './css/sei.css';
@@ -177,6 +178,7 @@ class Editor extends DecoupledEditor {
177
178
  SalvarComo,
178
179
  HeadingUiClassPlugin,
179
180
  ConteudoDinamicoPlugin,
181
+ DocumentosPlugin,
180
182
  // NumberedDivList,
181
183
  // NumberedDivListSplit
182
184
  ];
@@ -258,7 +260,8 @@ class Editor extends DecoupledEditor {
258
260
  'documentoModelo',
259
261
  'salvarcomo',
260
262
  // 'numberedDivListSplit'
261
- 'conteudoDinamico'
263
+ 'conteudoDinamico',
264
+ 'insertDocumento'
262
265
  ],
263
266
  shouldNotGroupWhenFull: true
264
267
  },
@@ -289,7 +292,22 @@ class Editor extends DecoupledEditor {
289
292
  startIndex: true,
290
293
  reversed: true
291
294
  }
292
- }
295
+ },
296
+ inserirDocumento: {
297
+ dropdownLabel: 'Inserir Documento',
298
+ getItems: async () => {
299
+ const lista = await buscarListaDocumentos();
300
+
301
+ return lista.map(doc => ({
302
+ id: doc.id,
303
+ label: doc.nome,
304
+ getHtml: async () => {
305
+ const conteudo = await buscarConteudoDocumento(doc.id);
306
+ return conteudo.html;
307
+ }
308
+ }));
309
+ }
310
+ }
293
311
  };
294
312
  }
295
313
 
@@ -78,4 +78,51 @@
78
78
  .num-list,
79
79
  .num-list > .num-li {
80
80
  text-indent: 0px !important;
81
+ }
82
+
83
+ .ck-conteudo-dinamico__loading {
84
+ display: inline-flex;
85
+ align-items: center;
86
+ gap: 10px;
87
+ padding: 12px 14px;
88
+ border: 1px solid #dcdcdc;
89
+ border-radius: 6px;
90
+ background: #fafafa;
91
+ color: #555;
92
+ min-height: 44px;
93
+ }
94
+
95
+ .ck-conteudo-dinamico__dots {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ gap: 4px;
99
+ height: 16px;
100
+ }
101
+
102
+ .ck-conteudo-dinamico__dots span {
103
+ width: 6px;
104
+ height: 6px;
105
+ border-radius: 50%;
106
+ background: #666;
107
+ display: inline-block;
108
+ animation: ck-dots-swing 1s infinite ease-in-out;
109
+ }
110
+
111
+ .ck-conteudo-dinamico__dots span:nth-child(2) {
112
+ animation-delay: 0.15s;
113
+ }
114
+
115
+ .ck-conteudo-dinamico__dots span:nth-child(3) {
116
+ animation-delay: 0.3s;
117
+ }
118
+
119
+ @keyframes ck-dots-swing {
120
+ 0%, 80%, 100% {
121
+ transform: translateY(0);
122
+ opacity: 0.45;
123
+ }
124
+ 40% {
125
+ transform: translateY(-4px);
126
+ opacity: 1;
127
+ }
81
128
  }
@@ -0,0 +1,389 @@
1
+ // @ts-nocheck
2
+ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
3
+ import Command from '@ckeditor/ckeditor5-core/src/command';
4
+
5
+ import Widget from '@ckeditor/ckeditor5-widget/src/widget';
6
+ import { toWidget, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget/src/utils';
7
+
8
+ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
9
+ import Model from '@ckeditor/ckeditor5-ui/src/model';
10
+ import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
11
+
12
+ export default class DocumentosPlugin extends Plugin {
13
+ static get pluginName() {
14
+ return 'DocumentosPlugin';
15
+ }
16
+
17
+ static get requires() {
18
+ return [ Widget ];
19
+ }
20
+
21
+ async init() {
22
+ const editor = this.editor;
23
+
24
+ this._lastHtmlBySourceId = new Map();
25
+ this._pendingUpdates = new Set();
26
+ this._itemsBySourceId = new Map();
27
+ this._getHtmlBySourceId = new Map();
28
+
29
+ const cfg = editor.config.get('inserirDocumento') || {};
30
+ const dropdownLabel = cfg.dropdownLabel || 'Inserir Documento';
31
+ const loadingLabel = cfg.loadingLabel || 'Carregando...';
32
+ const getItems = typeof cfg.getItems === 'function' ? cfg.getItems : null;
33
+
34
+ editor.model.schema.register('inserirDocumento', {
35
+ isObject: true,
36
+ isBlock: true,
37
+ allowWhere: '$block',
38
+ allowAttributes: [ 'sourceId', 'htmlB64' ]
39
+ });
40
+
41
+ editor.editing.mapper.on(
42
+ 'viewToModelPosition',
43
+ viewToModelPositionOutsideModelElement(
44
+ editor.model,
45
+ viewElement => viewElement.hasClass('ck-conteudo-dinamico')
46
+ )
47
+ );
48
+
49
+ editor.conversion.for('upcast').elementToElement({
50
+ view: {
51
+ name: 'div',
52
+ attributes: { 'data-conteudo-dinamico': '1' }
53
+ },
54
+ model: (viewElement, { writer }) => {
55
+ const sourceId = viewElement.getAttribute('data-source-id') || '';
56
+ const htmlB64 = viewElement.getAttribute('data-html') || '';
57
+
58
+ return writer.createElement('inserirDocumento', { sourceId, htmlB64 });
59
+ }
60
+ });
61
+
62
+ editor.conversion.for('dataDowncast').elementToElement({
63
+ model: 'inserirDocumento',
64
+ view: (modelItem, { writer }) => {
65
+ const sourceId = modelItem.getAttribute('sourceId') || '';
66
+ const htmlB64 = modelItem.getAttribute('htmlB64') || '';
67
+ const html = decodeHtml(htmlB64) || defaultHtml();
68
+
69
+ return writer.createRawElement(
70
+ 'div',
71
+ {
72
+ class: 'ck-conteudo-dinamico',
73
+ 'data-conteudo-dinamico': '1',
74
+ 'data-source-id': sourceId,
75
+ 'data-html': htmlB64
76
+ },
77
+ domElement => {
78
+ domElement.innerHTML = html;
79
+ }
80
+ );
81
+ }
82
+ });
83
+
84
+ editor.conversion.for('editingDowncast').elementToElement({
85
+ model: 'inserirDocumento',
86
+ view: (modelItem, { writer }) => {
87
+ const sourceId = modelItem.getAttribute('sourceId') || '';
88
+ const htmlB64 = modelItem.getAttribute('htmlB64') || '';
89
+ const html = decodeHtml(htmlB64) || defaultHtml();
90
+
91
+ const meta = this._itemsBySourceId.get(sourceId);
92
+ const widgetLabel = meta?.label || sourceId || 'Conteúdo dinâmico';
93
+
94
+ const container = writer.createContainerElement('div', {
95
+ class: 'ck-conteudo-dinamico',
96
+ 'data-conteudo-dinamico': '1',
97
+ 'data-source-id': sourceId
98
+ });
99
+
100
+ if (meta?.showLabel !== false) {
101
+ const header = writer.createContainerElement('div', {
102
+ class: 'ck-conteudo-dinamico__header'
103
+ });
104
+
105
+ if (meta?.iconSvg) {
106
+ const icon = writer.createRawElement(
107
+ 'span',
108
+ { class: 'ck-conteudo-dinamico__icon' },
109
+ domEl => {
110
+ domEl.innerHTML = meta.iconSvg;
111
+ }
112
+ );
113
+ writer.insert(writer.createPositionAt(header, 'end'), icon);
114
+ }
115
+
116
+ if (meta?.label) {
117
+ const title = writer.createContainerElement('span', {
118
+ class: 'ck-conteudo-dinamico__title'
119
+ });
120
+ writer.insert(writer.createPositionAt(title, 0), writer.createText(meta.label));
121
+ writer.insert(writer.createPositionAt(header, 'end'), title);
122
+ }
123
+
124
+ writer.insert(writer.createPositionAt(container, 'end'), header);
125
+ }
126
+
127
+ const raw = writer.createRawElement(
128
+ 'div',
129
+ { class: 'ck-conteudo-dinamico__inner' },
130
+ domElement => {
131
+ domElement.innerHTML = html;
132
+ }
133
+ );
134
+
135
+ writer.insert(writer.createPositionAt(container, 'end'), raw);
136
+
137
+ return toWidget(container, writer, { label: widgetLabel });
138
+ }
139
+ });
140
+
141
+ editor.commands.add('insertInserirDocumento', new InsertInserirDocumentoCommand(editor));
142
+
143
+ editor.model.document.on('change:data', () => {
144
+ if (!this._lastHtmlBySourceId.size) return;
145
+
146
+ for (const [ sourceId ] of this._lastHtmlBySourceId.entries()) {
147
+ this._pendingUpdates.add(sourceId);
148
+ }
149
+
150
+ flushPendingUpdates(editor, this);
151
+ });
152
+
153
+ let initialItems = [];
154
+
155
+ try {
156
+ if (getItems) {
157
+ const result = await getItems();
158
+ initialItems = Array.isArray(result) ? result : [];
159
+ }
160
+ } catch (error) {
161
+ console.error('Erro ao carregar lista de documentos na inicialização', error);
162
+ initialItems = [];
163
+ }
164
+
165
+ for (const item of initialItems) {
166
+ if (!item?.id) continue;
167
+
168
+ const sourceId = String(item.id);
169
+
170
+ this._itemsBySourceId.set(sourceId, {
171
+ sourceId,
172
+ label: item.label || sourceId,
173
+ iconSvg: item.iconSvg,
174
+ showLabel: item.showLabel
175
+ });
176
+
177
+ if (typeof item.getHtml === 'function') {
178
+ this._getHtmlBySourceId.set(sourceId, item.getHtml);
179
+ } else if (typeof item.html === 'string') {
180
+ this._getHtmlBySourceId.set(sourceId, async () => item.html);
181
+ } else if (typeof item.text === 'string') {
182
+ this._getHtmlBySourceId.set(sourceId, async () => `<p>${escapeHtml(item.text)}</p>`);
183
+ }
184
+ }
185
+
186
+ editor.ui.componentFactory.add('insertDocumento', locale => {
187
+ const dropdown = createDropdown(locale);
188
+
189
+ dropdown.buttonView.set({
190
+ label: dropdownLabel,
191
+ tooltip: true,
192
+ withText: true,
193
+ isVisible: initialItems.length > 0
194
+ });
195
+
196
+ const listItems = new Collection();
197
+ addListToDropdown(dropdown, listItems);
198
+
199
+ for (const item of initialItems) {
200
+ if (!item?.id) continue;
201
+
202
+ const sourceId = String(item.id);
203
+
204
+ listItems.add({
205
+ type: 'button',
206
+ model: new Model({
207
+ label: item.label || sourceId,
208
+ withText: true,
209
+ documentItemId: sourceId
210
+ })
211
+ });
212
+ }
213
+
214
+ dropdown.on('execute', async evt => {
215
+ const baseSourceId = evt.source.documentItemId;
216
+
217
+ if (!baseSourceId) return;
218
+
219
+ const getHtmlFn = this._getHtmlBySourceId.get(baseSourceId);
220
+ if (!getHtmlFn) return;
221
+
222
+ const sourceId = createInstanceSourceId(baseSourceId);
223
+ const meta = this._itemsBySourceId.get(baseSourceId);
224
+
225
+ if (meta) {
226
+ this._itemsBySourceId.set(sourceId, {
227
+ ...meta,
228
+ sourceId
229
+ });
230
+ }
231
+
232
+ try {
233
+ editor.execute('insertInserirDocumento', {
234
+ sourceId,
235
+ html: loadingDotsHtml('Aguarde...')
236
+ });
237
+ editor.editing.view.focus();
238
+
239
+ const html = await getHtmlFn();
240
+
241
+ this.updateBySourceId(sourceId, html);
242
+ } catch (error) {
243
+ console.error('Erro ao carregar conteúdo do documento', error);
244
+
245
+ this.updateBySourceId(
246
+ sourceId,
247
+ `
248
+ <div class="ck-conteudo-dinamico__error" contenteditable="false">
249
+ Não foi possível carregar o conteúdo.
250
+ </div>
251
+ `
252
+ );
253
+ }
254
+ });
255
+
256
+ return dropdown;
257
+ });
258
+
259
+ this.updateBySourceId = (sourceId, html) => {
260
+ this._lastHtmlBySourceId.set(sourceId, html);
261
+ this._pendingUpdates.add(sourceId);
262
+ flushPendingUpdates(editor, this);
263
+ };
264
+
265
+ function flushPendingUpdates(editor, plugin) {
266
+ if (!plugin._pendingUpdates.size) return;
267
+
268
+ const ids = Array.from(plugin._pendingUpdates);
269
+ plugin._pendingUpdates.clear();
270
+
271
+ for (const sourceId of ids) {
272
+ const html = plugin._lastHtmlBySourceId.get(sourceId);
273
+ if (html == null) continue;
274
+ updateConteudo(editor, sourceId, html);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ class InsertInserirDocumentoCommand extends Command {
281
+ execute({ sourceId, html }) {
282
+ const editor = this.editor;
283
+ const htmlB64 = encodeHtml(html);
284
+
285
+ editor.model.change(writer => {
286
+ const element = writer.createElement('inserirDocumento', {
287
+ sourceId,
288
+ htmlB64
289
+ });
290
+
291
+ editor.model.insertContent(element);
292
+ writer.setSelection(element, 'on');
293
+ });
294
+ }
295
+
296
+ refresh() {
297
+ const model = this.editor.model;
298
+ const selection = model.document.selection;
299
+ const allowedIn = model.schema.findAllowedParent(
300
+ selection.getFirstPosition(),
301
+ 'inserirDocumento'
302
+ );
303
+
304
+ this.isEnabled = !!allowedIn;
305
+ }
306
+ }
307
+
308
+ function updateConteudo(editor, sourceId, html) {
309
+ const newHtmlB64 = encodeHtml(html);
310
+
311
+ editor.model.change(writer => {
312
+ const root = editor.model.document.getRoot();
313
+
314
+ const targets = [];
315
+ for (const item of editor.model.createRangeIn(root).getItems()) {
316
+ if (
317
+ item.is('element', 'inserirDocumento') &&
318
+ item.getAttribute('sourceId') === sourceId
319
+ ) {
320
+ targets.push(item);
321
+ }
322
+ }
323
+
324
+ for (const item of targets) {
325
+ const currentB64 = item.getAttribute('htmlB64') || '';
326
+ if (currentB64 === newHtmlB64) continue;
327
+
328
+ const wasSelected = editor.model.document.selection.getSelectedElement() === item;
329
+ const insertPos = writer.createPositionBefore(item);
330
+
331
+ const attrs = Object.fromEntries(item.getAttributes());
332
+ attrs.htmlB64 = newHtmlB64;
333
+
334
+ writer.remove(item);
335
+
336
+ const newElement = writer.createElement('inserirDocumento', attrs);
337
+ writer.insert(newElement, insertPos);
338
+
339
+ if (wasSelected) {
340
+ writer.setSelection(newElement, 'on');
341
+ }
342
+ }
343
+ });
344
+ }
345
+
346
+ function defaultHtml() {
347
+ return '';
348
+ }
349
+
350
+ function encodeHtml(str) {
351
+ try {
352
+ return btoa(unescape(encodeURIComponent(str)));
353
+ } catch {
354
+ return '';
355
+ }
356
+ }
357
+
358
+ function decodeHtml(b64) {
359
+ if (!b64) return '';
360
+ try {
361
+ return decodeURIComponent(escape(atob(b64)));
362
+ } catch {
363
+ return '';
364
+ }
365
+ }
366
+
367
+ function escapeHtml(str = '') {
368
+ return String(str)
369
+ .replaceAll('&', '&amp;')
370
+ .replaceAll('<', '&lt;')
371
+ .replaceAll('>', '&gt;')
372
+ .replaceAll('"', '&quot;')
373
+ .replaceAll("'", '&#39;');
374
+ }
375
+
376
+ function loadingDotsHtml(label = '') {
377
+ return `
378
+ <div class="ck-conteudo-dinamico__loading" contenteditable="false">
379
+ <span class="ck-conteudo-dinamico__dots" aria-hidden="true">
380
+ <span></span><span></span><span></span>
381
+ </span>
382
+ <span class="ck-conteudo-dinamico__loading-text">${escapeHtml(label)}</span>
383
+ </div>
384
+ `;
385
+ }
386
+
387
+ function createInstanceSourceId(baseId) {
388
+ return `${baseId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
389
+ }