| @@ -4,8 +4,12 @@ | ||
| 4 | 4 | |
| 5 | 5 | const SECOND_IN_MS = 1000 |
| 6 | 6 | const TIMEOUT_SECONDS = 40 |
| 7 | 7 | const JSON_INDENT_SPACES = 2 |
| 8 | +const EXPORT_FILENAME = 'snippets' | |
| 9 | +const EXPORT_GENERATOR = 'Code Snippets' | |
| 10 | +const DEFAULT_PRIORITY = 10 | |
| 11 | +const EXPORT_DATE_LENGTH = 16 | |
| 8 | 12 | |
| 9 | 13 | const MIME_INFO = <const> { |
| 10 | 14 | php: ['php', 'text/php'], |
| 11 | 15 | html: ['php', 'text/php'], |
| @@ -20,9 +24,18 @@ | ||
| 20 | 24 | link.download = filename |
| 21 | 25 | link.href = URL.createObjectURL(new Blob([content], { type })) |
| 22 | 26 | |
| 23 | 27 | setTimeout(() => URL.revokeObjectURL(link.href), TIMEOUT_SECONDS * SECOND_IN_MS) |
| 24 | - setTimeout(() => link.click(), 0) | |
| 28 | + | |
| 29 | + // Some browsers (notably headless Chromium) can ignore programmatic clicks on detached anchors. | |
| 30 | + // Appending the link to the DOM before clicking improves reliability. | |
| 31 | + link.style.display = 'none' | |
| 32 | + document.body.appendChild(link) | |
| 33 | + | |
| 34 | + setTimeout(() => { | |
| 35 | + link.click() | |
| 36 | + link.remove() | |
| 37 | + }, 0) | |
| 25 | 38 | } |
| 26 | 39 | |
| 27 | 40 | export const downloadSnippetExportFile = ( |
| 28 | 41 | content: SnippetsExport | string, |
| @@ -39,5 +52,91 @@ | ||
| 39 | 52 | } else { |
| 40 | 53 | const filename = `${title}.code-snippets.json` |
| 41 | 54 | downloadAsFile(JSON.stringify(content, undefined, JSON_INDENT_SPACES), filename, 'application/json') |
| 42 | 55 | } |
| 56 | +} | |
| 57 | + | |
| 58 | +type DefaultExportValueCheck = (value: unknown) => boolean | |
| 59 | + | |
| 60 | +const isNullish = (value: unknown): value is undefined | null => undefined === value || null === value | |
| 61 | + | |
| 62 | +const DEFAULT_EXPORT_VALUE_CHECKS: Record<string, DefaultExportValueCheck> = { | |
| 63 | + id: value => 0 === value, | |
| 64 | + desc: value => '' === value, | |
| 65 | + name: value => '' === value, | |
| 66 | + code: value => '' === value, | |
| 67 | + tags: value => Array.isArray(value) && 0 === value.length, | |
| 68 | + scope: value => 'global' === value, | |
| 69 | + condition_id: value => 0 === value, | |
| 70 | + active: value => false === value, | |
| 71 | + locked: value => false === value, | |
| 72 | + trashed: value => false === value, | |
| 73 | + priority: value => DEFAULT_PRIORITY === value, | |
| 74 | + modified: value => '' === value || isNullish(value), | |
| 75 | + last_active: value => 0 === value || isNullish(value), | |
| 76 | + network: value => null === value || false === value, | |
| 77 | + shared_network: value => null === value || false === value, | |
| 78 | + code_error: value => null === value || false === value, | |
| 79 | + code_error_trace: value => null === value || false === value | |
| 80 | +} | |
| 81 | + | |
| 82 | +const isDefaultExportValue = (field: string, value: unknown): boolean => | |
| 83 | + (DEFAULT_EXPORT_VALUE_CHECKS[field] ?? isNullish)(value) | |
| 84 | + | |
| 85 | +const buildExportSnippet = ({ | |
| 86 | + id, | |
| 87 | + name, | |
| 88 | + desc, | |
| 89 | + code, | |
| 90 | + tags, | |
| 91 | + scope, | |
| 92 | + active, | |
| 93 | + locked, | |
| 94 | + trashed, | |
| 95 | + network, | |
| 96 | + shared_network, | |
| 97 | + modified, | |
| 98 | + priority, | |
| 99 | + conditionId, | |
| 100 | + lastActive, | |
| 101 | + code_error, | |
| 102 | + code_error_trace | |
| 103 | +}: Snippet): SnippetsExport['snippets'][number] => | |
| 104 | + Object.fromEntries( | |
| 105 | + Object.entries({ | |
| 106 | + id, | |
| 107 | + name, | |
| 108 | + desc, | |
| 109 | + code, | |
| 110 | + tags, | |
| 111 | + scope, | |
| 112 | + active, | |
| 113 | + locked, | |
| 114 | + trashed, | |
| 115 | + network, | |
| 116 | + shared_network, | |
| 117 | + modified, | |
| 118 | + priority, | |
| 119 | + condition_id: conditionId, | |
| 120 | + last_active: lastActive, | |
| 121 | + code_error, | |
| 122 | + code_error_trace | |
| 123 | + }).filter(([field, value]) => !isDefaultExportValue(field, value)) | |
| 124 | + ) | |
| 125 | + | |
| 126 | +export const downloadBulkSnippetExportFile = (snippets: readonly Snippet[]) => { | |
| 127 | + if (0 === snippets.length) { | |
| 128 | + return | |
| 129 | + } | |
| 130 | + | |
| 131 | + const content: SnippetsExport = { | |
| 132 | + generator: EXPORT_GENERATOR, | |
| 133 | + date_created: new Date().toISOString().slice(0, EXPORT_DATE_LENGTH).replace('T', ' '), | |
| 134 | + snippets: snippets.map(buildExportSnippet) | |
| 135 | + } | |
| 136 | + | |
| 137 | + downloadAsFile( | |
| 138 | + JSON.stringify(content, undefined, JSON_INDENT_SPACES), | |
| 139 | + `${EXPORT_FILENAME}.code-snippets.json`, | |
| 140 | + 'application/json' | |
| 141 | + ) | |
| 43 | 142 | } |