quill-table-up 3.3.2 → 3.5.0
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/README.md +54 -10
- package/dist/index.d.ts +15 -30
- package/dist/index.js +29 -29
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +27 -27
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/unit/table-clipboard.test.ts +228 -0
- package/src/__tests__/unit/table-refresh-ui.test.ts +50 -0
- package/src/__tests__/unit/utils.ts +2 -2
- package/src/modules/table-clipboard/table-clipboard.ts +14 -2
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/table-up.ts +64 -29
- package/src/utils/index.ts +1 -1
- package/src/utils/{style-helper.ts → style/helper.ts} +1 -1
- package/src/utils/style/index.ts +2 -0
- package/src/utils/style/inline-core.ts +80 -0
- package/src/utils/types.ts +6 -0
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import Quill from 'quill';
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import { TableCellInnerFormat } from '../../formats';
|
|
4
4
|
import { TableUp } from '../../table-up';
|
|
5
|
+
import { parseCSSRules } from '../../utils';
|
|
5
6
|
import { createQuillWithTableModule, createTableBodyHTML, createTableCaptionHTML, createTableDeltaOps, createTableHTML, createTaleColHTML, datasetTag, expectDelta, replaceAttrEmptyRow, simulatePasteHTML } from './utils';
|
|
6
7
|
|
|
7
8
|
const Delta = Quill.import('delta');
|
|
@@ -2174,3 +2175,230 @@ describe('test TableUp `getHTMLByCell`', () => {
|
|
|
2174
2175
|
}
|
|
2175
2176
|
});
|
|
2176
2177
|
});
|
|
2178
|
+
|
|
2179
|
+
describe('clipboard style block resolution', () => {
|
|
2180
|
+
it('class-based background-color is preserved on table cell', () => {
|
|
2181
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2182
|
+
const delta = quill.clipboard.convert({
|
|
2183
|
+
html: '<style>.xl65{background-color:#FFC000}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2184
|
+
});
|
|
2185
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2186
|
+
expect(cellOp).toBeDefined();
|
|
2187
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2188
|
+
expect(style).toContain('background-color');
|
|
2189
|
+
expect(style).toMatch(/#FFC000|rgb\(255, 192, 0\)/i);
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
it('class-based color is preserved as Quill inline format', () => {
|
|
2193
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2194
|
+
const delta = quill.clipboard.convert({
|
|
2195
|
+
html: '<style>.xl65{color:red}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2196
|
+
});
|
|
2197
|
+
// text and \n may be merged into a single op when attributes match
|
|
2198
|
+
const textOp = delta.ops.find(op => typeof op.insert === 'string' && (op.insert as string).includes('text'));
|
|
2199
|
+
expect(textOp).toBeDefined();
|
|
2200
|
+
expect(textOp!.attributes?.color).toBeTruthy();
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
it('class-based bold is preserved as Quill inline format', () => {
|
|
2204
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2205
|
+
const delta = quill.clipboard.convert({
|
|
2206
|
+
html: '<style>.xl65{font-weight:bold}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2207
|
+
});
|
|
2208
|
+
// text and \n may be merged into a single op when attributes match
|
|
2209
|
+
const textOp = delta.ops.find(op => typeof op.insert === 'string' && (op.insert as string).includes('text'));
|
|
2210
|
+
expect(textOp).toBeDefined();
|
|
2211
|
+
expect(textOp!.attributes?.bold).toBe(true);
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
it('inline style takes priority over class style', () => {
|
|
2215
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2216
|
+
const delta = quill.clipboard.convert({
|
|
2217
|
+
html: '<style>.xl65{background-color:red}</style><table><tr><td class="xl65" style="background-color:blue">text</td></tr></table>',
|
|
2218
|
+
});
|
|
2219
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2220
|
+
expect(cellOp).toBeDefined();
|
|
2221
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2222
|
+
expect(style).toContain('background-color');
|
|
2223
|
+
expect(style).toMatch(/blue/i);
|
|
2224
|
+
expect(style).not.toMatch(/red/i);
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
it('@-rules are skipped without error', () => {
|
|
2228
|
+
const rules = parseCSSRules('@page{margin:1in} .xl65{background:red}');
|
|
2229
|
+
expect(rules.length).toBe(1);
|
|
2230
|
+
expect(rules[0].selector).toBe('.xl65');
|
|
2231
|
+
expect(rules[0].styles.background).toBe('red');
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
it('multiple style blocks are all processed', () => {
|
|
2235
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2236
|
+
const delta = quill.clipboard.convert({
|
|
2237
|
+
html: '<style>.a{background-color:red}</style><style>.b{background-color:blue}</style><table><tr><td class="a">A</td><td class="b">B</td></tr></table>',
|
|
2238
|
+
});
|
|
2239
|
+
const cellOps = delta.ops.filter(op => op.attributes?.['table-up-cell-inner']);
|
|
2240
|
+
expect(cellOps.length).toBeGreaterThanOrEqual(2);
|
|
2241
|
+
expect((cellOps[0].attributes!['table-up-cell-inner'] as any).style).toContain('background-color');
|
|
2242
|
+
expect((cellOps[1].attributes!['table-up-cell-inner'] as any).style).toContain('background-color');
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
it('simple tag selectors (td) are skipped by default', () => {
|
|
2246
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2247
|
+
const delta = quill.clipboard.convert({
|
|
2248
|
+
html: '<style>td{background-color:red} .xl65{background-color:blue}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2249
|
+
});
|
|
2250
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2251
|
+
expect(cellOp).toBeDefined();
|
|
2252
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2253
|
+
// class selector applied, tag selector skipped
|
|
2254
|
+
expect(style).toMatch(/blue/i);
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
it('descendant tag selectors (table td) are skipped by default', () => {
|
|
2258
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2259
|
+
const delta = quill.clipboard.convert({
|
|
2260
|
+
html: '<style>table td{background-color:red} .xl65{background-color:blue}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2261
|
+
});
|
|
2262
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2263
|
+
expect(cellOp).toBeDefined();
|
|
2264
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2265
|
+
expect(style).toMatch(/blue/i);
|
|
2266
|
+
expect(style).not.toMatch(/red/i);
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
it('child combinator tag selectors (tr > td) are skipped by default', () => {
|
|
2270
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2271
|
+
const delta = quill.clipboard.convert({
|
|
2272
|
+
html: '<style>tr > td{background-color:red}</style><table><tr><td>text</td></tr></table>',
|
|
2273
|
+
});
|
|
2274
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2275
|
+
expect(cellOp).toBeDefined();
|
|
2276
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2277
|
+
expect(style).not.toContain('background-color');
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
it('deep combinator tag selectors (table > tbody > tr > td) are skipped by default', () => {
|
|
2281
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2282
|
+
const delta = quill.clipboard.convert({
|
|
2283
|
+
html: '<style>table > tbody > tr > td{background-color:red}</style><table><tbody><tr><td>text</td></tr></tbody></table>',
|
|
2284
|
+
});
|
|
2285
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2286
|
+
expect(cellOp).toBeDefined();
|
|
2287
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2288
|
+
expect(style).not.toContain('background-color');
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
it('sibling combinator tag selectors (td + td) are skipped by default', () => {
|
|
2292
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2293
|
+
const delta = quill.clipboard.convert({
|
|
2294
|
+
html: '<style>td + td{background-color:red}</style><table><tr><td>a</td><td>b</td></tr></table>',
|
|
2295
|
+
});
|
|
2296
|
+
const cellOps = delta.ops.filter(op => op.attributes?.['table-up-cell-inner']);
|
|
2297
|
+
for (const op of cellOps) {
|
|
2298
|
+
const style = (op.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2299
|
+
expect(style).not.toContain('background-color');
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
it('tag+class selectors (td.xl65) are NOT skipped', () => {
|
|
2304
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2305
|
+
const delta = quill.clipboard.convert({
|
|
2306
|
+
html: '<style>td.xl65{background-color:red}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2307
|
+
});
|
|
2308
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2309
|
+
expect(cellOp).toBeDefined();
|
|
2310
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2311
|
+
expect(style).toContain('background-color');
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
it('tag+id selectors (td#main) are NOT skipped', () => {
|
|
2315
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2316
|
+
const delta = quill.clipboard.convert({
|
|
2317
|
+
html: '<style>td#main{background-color:red}</style><table><tr><td id="main">text</td></tr></table>',
|
|
2318
|
+
});
|
|
2319
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2320
|
+
expect(cellOp).toBeDefined();
|
|
2321
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2322
|
+
expect(style).toContain('background-color');
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
it('tag+attribute selectors (td[data-x]) are NOT skipped', () => {
|
|
2326
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2327
|
+
const delta = quill.clipboard.convert({
|
|
2328
|
+
html: '<style>td[data-x]{background-color:red}</style><table><tr><td data-x="1">text</td></tr></table>',
|
|
2329
|
+
});
|
|
2330
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2331
|
+
expect(cellOp).toBeDefined();
|
|
2332
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2333
|
+
expect(style).toContain('background-color');
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
it('class descendant tag selectors (.cls td) are NOT skipped', () => {
|
|
2337
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2338
|
+
const delta = quill.clipboard.convert({
|
|
2339
|
+
html: '<style>.wrap td{background-color:#FFC000}</style><table class="wrap"><tr><td>text</td></tr></table>',
|
|
2340
|
+
});
|
|
2341
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2342
|
+
expect(cellOp).toBeDefined();
|
|
2343
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2344
|
+
expect(style).toContain('background-color');
|
|
2345
|
+
expect(style).toMatch(/#FFC000|rgb\(255, 192, 0\)/i);
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
it('pseudo selectors (td:first-child) are NOT skipped', () => {
|
|
2349
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2350
|
+
const delta = quill.clipboard.convert({
|
|
2351
|
+
html: '<style>td:first-child{background-color:red}</style><table><tr><td>a</td><td>b</td></tr></table>',
|
|
2352
|
+
});
|
|
2353
|
+
const cellOps = delta.ops.filter(op => op.attributes?.['table-up-cell-inner']);
|
|
2354
|
+
// first cell should have background-color, second should not
|
|
2355
|
+
const styles = cellOps.map(op => (op.attributes!['table-up-cell-inner'] as any).style || '');
|
|
2356
|
+
expect(styles.some(s => s.includes('background-color'))).toBe(true);
|
|
2357
|
+
expect(styles.some(s => !s.includes('background-color'))).toBe(true);
|
|
2358
|
+
});
|
|
2359
|
+
|
|
2360
|
+
it('comma-separated mixed selectors skip only the tag-only parts', () => {
|
|
2361
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2362
|
+
const delta = quill.clipboard.convert({
|
|
2363
|
+
html: '<style>td, .xl65{background-color:red}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2364
|
+
});
|
|
2365
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2366
|
+
expect(cellOp).toBeDefined();
|
|
2367
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2368
|
+
// ".xl65" part applied (tag "td" part skipped, but .xl65 matches the same element)
|
|
2369
|
+
expect(style).toContain('background-color');
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
it('complex tag selectors are applied when pasteDefaultTagStyle is true', () => {
|
|
2373
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true, pasteDefaultTagStyle: true });
|
|
2374
|
+
const delta = quill.clipboard.convert({
|
|
2375
|
+
html: '<style>table td{background-color:red}</style><table><tr><td>text</td></tr></table>',
|
|
2376
|
+
});
|
|
2377
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2378
|
+
expect(cellOp).toBeDefined();
|
|
2379
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2380
|
+
expect(style).toContain('background-color');
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
it('pasteStyleSheet=false disables style resolution', () => {
|
|
2384
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: false });
|
|
2385
|
+
const delta = quill.clipboard.convert({
|
|
2386
|
+
html: '<style>.xl65{background-color:#FFC000}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2387
|
+
});
|
|
2388
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2389
|
+
expect(cellOp).toBeDefined();
|
|
2390
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2391
|
+
expect(style).not.toContain('background-color');
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
it('pasteDefaultTagStyle=true enables tag selector styles', () => {
|
|
2395
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true, pasteDefaultTagStyle: true });
|
|
2396
|
+
const delta = quill.clipboard.convert({
|
|
2397
|
+
html: '<style>td{background-color:#FFC000}</style><table><tr><td>text</td></tr></table>',
|
|
2398
|
+
});
|
|
2399
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2400
|
+
expect(cellOp).toBeDefined();
|
|
2401
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2402
|
+
expect(style).toContain('background-color');
|
|
2403
|
+
});
|
|
2404
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { TableUp } from '../../table-up';
|
|
3
|
+
import { createQuillWithTableModule } from './utils';
|
|
4
|
+
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('TableUp - refreshUI', () => {
|
|
13
|
+
it('should call texts function with key and use returned text', async () => {
|
|
14
|
+
let locale = 'en';
|
|
15
|
+
const keyCalls: string[] = [];
|
|
16
|
+
const quill = createQuillWithTableModule('<p><br></p>', {
|
|
17
|
+
texts: (key) => {
|
|
18
|
+
keyCalls.push(key);
|
|
19
|
+
if (key === 'customBtnText') return locale === 'en' ? 'Custom' : '自定义';
|
|
20
|
+
if (key === 'fullCheckboxText') return locale === 'en' ? 'Insert full width table' : '插入满宽表格';
|
|
21
|
+
return '';
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const tableModule = quill.getModule(TableUp.moduleName) as TableUp;
|
|
26
|
+
expect(tableModule.options.texts.customBtnText).toBe('Custom');
|
|
27
|
+
expect(tableModule.options.texts.fullCheckboxText).toBe('Insert full width table');
|
|
28
|
+
expect(keyCalls).toContain('customBtnText');
|
|
29
|
+
expect(keyCalls).toContain('fullCheckboxText');
|
|
30
|
+
locale = 'zh';
|
|
31
|
+
await tableModule.refreshUI();
|
|
32
|
+
expect(tableModule.options.texts.customBtnText).toBe('自定义');
|
|
33
|
+
expect(tableModule.options.texts.fullCheckboxText).toBe('插入满宽表格');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should bind this to TableUp instance when calling texts function', () => {
|
|
37
|
+
let boundThis: TableUp | undefined;
|
|
38
|
+
const quill = createQuillWithTableModule('<p><br></p>', {
|
|
39
|
+
texts(key) {
|
|
40
|
+
boundThis = this;
|
|
41
|
+
if (key === 'customBtnText') return 'Custom';
|
|
42
|
+
return '';
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const tableModule = quill.getModule(TableUp.moduleName) as TableUp;
|
|
47
|
+
expect(tableModule.options.texts.customBtnText).toBe('Custom');
|
|
48
|
+
expect(boundThis).toBe(tableModule);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Op, Delta as TypeDelta, Range as TypeRange } from 'quill';
|
|
2
|
-
import type { TableCaptionValue, TableColValue,
|
|
2
|
+
import type { TableCaptionValue, TableColValue, TableUpOptionsInput } from '../../utils';
|
|
3
3
|
import Quill from 'quill';
|
|
4
4
|
import { expect, vi } from 'vitest';
|
|
5
5
|
import { TableUp } from '../../table-up';
|
|
@@ -41,7 +41,7 @@ export function sortAttributes(element: HTMLElement) {
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
export function createQuillWithTableModule(html: string, tableOptions:
|
|
44
|
+
export function createQuillWithTableModule(html: string, tableOptions: TableUpOptionsInput = {}, moduleOptions = {}, quillOptions = {}, register = {}) {
|
|
45
45
|
Quill.register({
|
|
46
46
|
[`modules/${TableUp.moduleName}`]: TableUp,
|
|
47
47
|
...register,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Parchment as TypeParchment } from 'quill';
|
|
2
2
|
import type { Delta as TypeDelta } from 'quill/core';
|
|
3
3
|
import type TypeClipboard from 'quill/modules/clipboard';
|
|
4
|
-
import type {
|
|
4
|
+
import type { TableUp } from '../../table-up';
|
|
5
|
+
import type { TableCaptionValue, TableCellValue, TableUpOptions } from '../../utils';
|
|
5
6
|
import Quill from 'quill';
|
|
6
7
|
import { TableCellFormat, TableColFormat } from '../../formats';
|
|
7
|
-
import { blotName, cssTextToObject, isObject, isString, objectToCssText, randomId, tableUpSize } from '../../utils';
|
|
8
|
+
import { blotName, cssTextToObject, isObject, isString, objectToCssText, randomId, resolveStyleSheetToInline, tableUpInternal, tableUpSize } from '../../utils';
|
|
8
9
|
|
|
9
10
|
const Delta = Quill.import('delta');
|
|
10
11
|
const Clipboard = Quill.import('modules/clipboard') as typeof TypeClipboard;
|
|
@@ -71,6 +72,17 @@ export class TableClipboard extends Clipboard {
|
|
|
71
72
|
this.addMatcher(Node.ELEMENT_NODE, this.matchTdAttributor.bind(this));
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
normalizeHTML(doc: Document) {
|
|
76
|
+
super.normalizeHTML(doc);
|
|
77
|
+
const tableModule = this.quill.getModule(tableUpInternal.moduleName) as TableUp | undefined;
|
|
78
|
+
const options: Partial<TableUpOptions> = tableModule?.options ?? {};
|
|
79
|
+
if (options.pasteStyleSheet !== false) {
|
|
80
|
+
resolveStyleSheetToInline(doc, {
|
|
81
|
+
includeDefaultTagStyle: options.pasteDefaultTagStyle,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
getStyleBackgroundColor(node: Node, delta: TypeDelta) {
|
|
75
87
|
const backgroundColor = (node as HTMLElement).style.backgroundColor;
|
|
76
88
|
if (backgroundColor) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.0.18","results":[[":__tests__/unit/utils.test-d.ts",{"duration":0,"failed":false}],[":__tests__/unit/table-
|
|
1
|
+
{"version":"4.0.18","results":[[":__tests__/unit/utils.test-d.ts",{"duration":0,"failed":false}],[":__tests__/unit/table-clipboard.test.ts",{"duration":9695.598699999999,"failed":false}],[":__tests__/unit/table-redo-undo.test.ts",{"duration":17920.0041,"failed":false}],[":__tests__/unit/table-hack.test.ts",{"duration":3642.7681000000002,"failed":false}],[":__tests__/unit/table-cell-merge.test.ts",{"duration":4956.3197,"failed":false}],[":__tests__/unit/table-blots.test.ts",{"duration":4045.2762999999995,"failed":false}],[":__tests__/unit/table-insert.test.ts",{"duration":7073.0178,"failed":false}],[":__tests__/unit/utils.test.ts",{"duration":975.8723999999997,"failed":false}],[":__tests__/unit/table-caption.test.ts",{"duration":1513.6225000000004,"failed":false}],[":__tests__/unit/table-contenteditable.test.ts",{"duration":681.7878000000001,"failed":false}],[":__tests__/unit/table-remove.test.ts",{"duration":4171.0812,"failed":false}],[":__tests__/unit/table-refresh-ui.test.ts",{"duration":414.3294000000001,"failed":false}]]}
|
package/src/table-up.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Context } from 'quill/modules/keyboard';
|
|
|
5
5
|
import type TypeKeyboard from 'quill/modules/keyboard';
|
|
6
6
|
import type TypeToolbar from 'quill/modules/toolbar';
|
|
7
7
|
import type { TableSelection } from './modules';
|
|
8
|
-
import type { Constructor, QuillTheme, QuillThemePicker, TableBodyTag, TableCellValue, TableConstantsData, TableTextOptions, TableUpOptions } from './utils';
|
|
8
|
+
import type { Constructor, QuillTheme, QuillThemePicker, TableBodyTag, TableCellValue, TableConstantsData, TableTextOptions, TableTextOptionsInput, TableUpOptions, TableUpOptionsInput } from './utils';
|
|
9
9
|
import Quill from 'quill';
|
|
10
10
|
import { BlockEmbedOverride, BlockOverride, ContainerFormat, ScrollOverride, TableBodyFormat, TableCaptionFormat, TableCellFormat, TableCellInnerFormat, TableColFormat, TableColgroupFormat, TableFootFormat, TableHeadFormat, TableMainFormat, TableRowFormat, TableWrapperFormat } from './formats';
|
|
11
11
|
import { TableClipboard } from './modules';
|
|
@@ -277,6 +277,7 @@ export class TableUp {
|
|
|
277
277
|
|
|
278
278
|
quill: Quill;
|
|
279
279
|
options: TableUpOptions;
|
|
280
|
+
textOptionsInput: TableTextOptionsInput | undefined;
|
|
280
281
|
toolBox: HTMLDivElement;
|
|
281
282
|
fixTableByLisenter = debounce(this.balanceTables, 100);
|
|
282
283
|
selector?: HTMLElement;
|
|
@@ -288,33 +289,28 @@ export class TableUp {
|
|
|
288
289
|
return this.constructor;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
|
-
constructor(quill: Quill, options:
|
|
292
|
+
constructor(quill: Quill, options: TableUpOptionsInput) {
|
|
292
293
|
this.quill = quill;
|
|
294
|
+
this.textOptionsInput = options?.texts;
|
|
293
295
|
this.options = this.resolveOptions(options || {});
|
|
294
296
|
this.toolBox = this.initialContainer();
|
|
295
297
|
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const { leftLimited } = limitDomInViewPort(selectRect);
|
|
308
|
-
if (leftLimited) {
|
|
309
|
-
const labelRect = picker.label.getBoundingClientRect();
|
|
310
|
-
Object.assign(picker.options.style, { transform: `translateX(calc(-100% + ${labelRect.width}px))` });
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
Object.assign(picker.options.style, { transform: undefined });
|
|
314
|
-
}
|
|
315
|
-
});
|
|
298
|
+
const picker = this.getToolbarPicker();
|
|
299
|
+
if (picker) {
|
|
300
|
+
picker.label.innerHTML = this.options.icon;
|
|
301
|
+
this.buildCustomSelect(this.options.customSelect, picker);
|
|
302
|
+
picker.label.addEventListener('mousedown', () => {
|
|
303
|
+
if (!this.selector) return;
|
|
304
|
+
const selectRect = this.selector.getBoundingClientRect();
|
|
305
|
+
const { leftLimited } = limitDomInViewPort(selectRect);
|
|
306
|
+
if (leftLimited) {
|
|
307
|
+
const labelRect = picker.label.getBoundingClientRect();
|
|
308
|
+
Object.assign(picker.options.style, { transform: `translateX(calc(-100% + ${labelRect.width}px))` });
|
|
316
309
|
}
|
|
317
|
-
|
|
310
|
+
else {
|
|
311
|
+
Object.assign(picker.options.style, { transform: undefined });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
318
314
|
}
|
|
319
315
|
|
|
320
316
|
const keyboard = this.quill.getModule('keyboard') as TypeKeyboard;
|
|
@@ -367,20 +363,31 @@ export class TableUp {
|
|
|
367
363
|
}
|
|
368
364
|
}
|
|
369
365
|
|
|
370
|
-
|
|
366
|
+
getToolbarPicker() {
|
|
367
|
+
const toolbar = this.quill.getModule('toolbar') as TypeToolbar;
|
|
368
|
+
if (!toolbar || !(this.quill.theme as QuillTheme).pickers) return;
|
|
369
|
+
const [, select] = (toolbar.controls as [string, HTMLElement][] || []).find(([name]) => name === this.statics.toolName) || [];
|
|
370
|
+
if (select?.tagName.toLocaleLowerCase() !== 'select') return;
|
|
371
|
+
return (this.quill.theme as QuillTheme).pickers.find(picker => picker.select === select);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
resolveOptions(options: TableUpOptionsInput): TableUpOptions {
|
|
375
|
+
const { texts, ...rest } = options;
|
|
371
376
|
return Object.assign({
|
|
372
377
|
customBtn: false,
|
|
373
|
-
texts: this.resolveTexts(
|
|
378
|
+
texts: this.resolveTexts(texts),
|
|
374
379
|
full: false,
|
|
375
380
|
fullSwitch: true,
|
|
376
381
|
icon: icons.table,
|
|
377
382
|
autoMergeCell: true,
|
|
383
|
+
pasteStyleSheet: false,
|
|
384
|
+
pasteDefaultTagStyle: false,
|
|
378
385
|
modules: [],
|
|
379
|
-
} as TableUpOptions,
|
|
386
|
+
} as TableUpOptions, rest);
|
|
380
387
|
}
|
|
381
388
|
|
|
382
|
-
resolveTexts(options
|
|
383
|
-
|
|
389
|
+
resolveTexts(options?: TableTextOptionsInput) {
|
|
390
|
+
const defaults: TableTextOptions = {
|
|
384
391
|
fullCheckboxText: 'Insert full width table',
|
|
385
392
|
customBtnText: 'Custom',
|
|
386
393
|
confirmText: 'Confirm',
|
|
@@ -405,7 +412,35 @@ export class TableUp {
|
|
|
405
412
|
DeleteTable: 'Delete table',
|
|
406
413
|
BackgroundColor: 'Set background color',
|
|
407
414
|
BorderColor: 'Set border color',
|
|
408
|
-
}
|
|
415
|
+
};
|
|
416
|
+
if (isFunction(options)) {
|
|
417
|
+
const textGetter = options;
|
|
418
|
+
const tableModule = this;
|
|
419
|
+
return new Proxy(defaults, {
|
|
420
|
+
get(target, key: string | symbol) {
|
|
421
|
+
if (typeof key !== 'string') return Reflect.get(target, key);
|
|
422
|
+
const value = textGetter.call(tableModule, key);
|
|
423
|
+
return isString(value) ? value : (target as Record<string, string>)[key];
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return Object.assign(defaults, options);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async refreshUI() {
|
|
431
|
+
this.options.texts = this.resolveTexts(this.textOptionsInput);
|
|
432
|
+
|
|
433
|
+
const picker = this.getToolbarPicker();
|
|
434
|
+
if (picker) {
|
|
435
|
+
picker.label.innerHTML = this.options.icon;
|
|
436
|
+
await this.buildCustomSelect(this.options.customSelect, picker);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const module of Object.values(this.modules)) {
|
|
440
|
+
if (module && typeof (module as any).hide === 'function') {
|
|
441
|
+
(module as any).hide();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
409
444
|
}
|
|
410
445
|
|
|
411
446
|
initModules() {
|
package/src/utils/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export * from './is';
|
|
|
8
8
|
export * from './position';
|
|
9
9
|
export * from './resize-observer-helper';
|
|
10
10
|
export * from './scroll';
|
|
11
|
-
export * from './style
|
|
11
|
+
export * from './style';
|
|
12
12
|
export * from './transformer';
|
|
13
13
|
export * from './transition-event-helper';
|
|
14
14
|
export * from './types';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
interface CSSRule {
|
|
2
|
+
selector: string;
|
|
3
|
+
styles: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function parseCSSRules(cssText: string): CSSRule[] {
|
|
7
|
+
const rules: CSSRule[] = [];
|
|
8
|
+
// Remove CSS comments
|
|
9
|
+
const cleaned = cssText.replaceAll(/\/\*[\s\S]*?\*\//g, '');
|
|
10
|
+
// Match selector { declarations } blocks
|
|
11
|
+
const ruleRegex = /([^{}]+)\{([^{}]*)\}/g;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = ruleRegex.exec(cleaned)) !== null) {
|
|
14
|
+
const selector = match[1].trim();
|
|
15
|
+
const declarations = match[2].trim();
|
|
16
|
+
// Skip @-rules
|
|
17
|
+
if (selector.startsWith('@')) continue;
|
|
18
|
+
const styles: Record<string, string> = {};
|
|
19
|
+
for (const decl of declarations.split(';')) {
|
|
20
|
+
const colonIndex = decl.indexOf(':');
|
|
21
|
+
if (colonIndex === -1) continue;
|
|
22
|
+
const prop = decl.slice(0, colonIndex).trim();
|
|
23
|
+
const value = decl.slice(colonIndex + 1).trim();
|
|
24
|
+
if (prop && value) {
|
|
25
|
+
styles[prop] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (Object.keys(styles).length > 0) {
|
|
29
|
+
rules.push({ selector, styles });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return rules;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A selector is tag-only if it contains no class (.), id (#),
|
|
36
|
+
// attribute ([), or pseudo (:) indicators — meaning it's composed
|
|
37
|
+
// solely of tag names and combinators (space, >, +, ~).
|
|
38
|
+
// e.g. "td", "table td", "tr > td", "table > tbody > tr > td"
|
|
39
|
+
export function isTagOnlySelector(selector: string): boolean {
|
|
40
|
+
return !/[.#\[:]/.test(selector);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ResolveStyleSheetOptions {
|
|
44
|
+
includeDefaultTagStyle?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveStyleSheetToInline(doc: Document, options?: ResolveStyleSheetOptions): void {
|
|
48
|
+
const includeDefaultTagStyle = options?.includeDefaultTagStyle ?? false;
|
|
49
|
+
const styleElements = doc.querySelectorAll('style');
|
|
50
|
+
for (const styleEl of Array.from(styleElements)) {
|
|
51
|
+
const rules = parseCSSRules(styleEl.textContent || '');
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
// A single selector string may contain comma-separated selectors
|
|
54
|
+
const selectors = rule.selector.split(',').map(s => s.trim()).filter(Boolean);
|
|
55
|
+
for (const selector of selectors) {
|
|
56
|
+
// Skip tag-only selectors (e.g. "td", "table td", "tr > td") unless explicitly allowed
|
|
57
|
+
if (!includeDefaultTagStyle && isTagOnlySelector(selector)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let elements: NodeListOf<Element>;
|
|
61
|
+
try {
|
|
62
|
+
elements = doc.querySelectorAll(selector);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Skip invalid selectors
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const el of Array.from(elements)) {
|
|
69
|
+
const htmlEl = el as HTMLElement;
|
|
70
|
+
for (const [prop, value] of Object.entries(rule.styles)) {
|
|
71
|
+
// Inline style takes priority — only set if not already present
|
|
72
|
+
if (!htmlEl.style.getPropertyValue(prop)) {
|
|
73
|
+
htmlEl.style.setProperty(prop, value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/utils/types.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface TableTextOptions extends TableCreatorTextOptions, TableMenuText
|
|
|
57
57
|
transparent: string;
|
|
58
58
|
perWidthInsufficient: string;
|
|
59
59
|
}
|
|
60
|
+
export type TableTextOptionsInput = Partial<TableTextOptions> | ((this: TableUp, key: string) => string);
|
|
60
61
|
export interface TableUpExtraModule extends Constructor<any, [TableUp, Quill, any]> {
|
|
61
62
|
moduleName: string;
|
|
62
63
|
}
|
|
@@ -72,8 +73,13 @@ export interface TableUpOptions {
|
|
|
72
73
|
texts: TableTextOptions;
|
|
73
74
|
icon: string;
|
|
74
75
|
autoMergeCell: boolean;
|
|
76
|
+
pasteStyleSheet: boolean;
|
|
77
|
+
pasteDefaultTagStyle: boolean;
|
|
75
78
|
modules: TableUpModule[];
|
|
76
79
|
}
|
|
80
|
+
export interface TableUpOptionsInput extends Partial<Omit<TableUpOptions, 'texts'>> {
|
|
81
|
+
texts?: TableTextOptionsInput;
|
|
82
|
+
}
|
|
77
83
|
export interface TableColValue {
|
|
78
84
|
tableId: string;
|
|
79
85
|
colId: string;
|