|
| 1 | +import type { ToolbarButtonOptions, ToolButtonOption } from '../options' |
| 2 | +import { isBoolean, isObject } from '../../../utils/is' |
| 3 | +import { CENTER_ALIGN, COPY, DOWNLOAD, LEFT_ALIGN, RIGHT_ALIGN } from '../options' |
| 4 | + |
| 5 | +export const ALIGN_ATTR = 'data-align' |
| 6 | + |
| 7 | +export function setAlignStyle(el: HTMLElement, display: string | null, float: string | null, margin: string | null) { |
| 8 | + el.style.setProperty('display', display) |
| 9 | + el.style.setProperty('float', float) |
| 10 | + el.style.setProperty('margin', margin) |
| 11 | +} |
| 12 | +export const alignmentHandler = { |
| 13 | + left: (el: HTMLElement, toolbarButtons: ImageToolbarButtons) => { |
| 14 | + setAlignStyle(el, 'inline', 'left', '0 1em 1em 0') |
| 15 | + }, |
| 16 | + center: (el: HTMLElement, toolbarButtons: ImageToolbarButtons) => { |
| 17 | + setAlignStyle(el, 'block', null, 'auto') |
| 18 | + }, |
| 19 | + right: (el: HTMLElement, toolbarButtons: ImageToolbarButtons) => { |
| 20 | + setAlignStyle(el, 'inline', 'right', '0 0 1em 1em') |
| 21 | + }, |
| 22 | + download: (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 23 | + const imageName = el.dataset.title || 'image' |
| 24 | + const url = el.src || '' |
| 25 | + if (!url) return |
| 26 | + const a = document.createElement('a') |
| 27 | + a.href = url |
| 28 | + a.target = '_blank' |
| 29 | + a.download = imageName |
| 30 | + a.style.display = 'none' |
| 31 | + document.body.appendChild(a) |
| 32 | + a.click() |
| 33 | + a.parentNode.removeChild(a) |
| 34 | + }, |
| 35 | + copy: async (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 36 | + if (!el.src) return |
| 37 | + const imageUrl = el.src |
| 38 | + try { |
| 39 | + const response = await fetch(imageUrl) |
| 40 | + if (!response.ok) { |
| 41 | + throw new Error('Copy image failed') |
| 42 | + } |
| 43 | + const blob = await response.blob() |
| 44 | + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]) |
| 45 | + } |
| 46 | + catch (e) { |
| 47 | + throw new Error('Copy image failed') |
| 48 | + } |
| 49 | + }, |
| 50 | +} |
| 51 | +const defaultButtons: Record<string, ToolButtonOption> = { |
| 52 | + [LEFT_ALIGN]: { |
| 53 | + name: LEFT_ALIGN, |
| 54 | + icon: ` |
| 55 | + <svg viewbox="0 0 18 18"> |
| 56 | + <line class="ql-stroke" x1="3" x2="15" y1="9" y2="9"></line> |
| 57 | + <line class="ql-stroke" x1="3" x2="13" y1="14" y2="14"></line> |
| 58 | + <line class="ql-stroke" x1="3" x2="9" y1="4" y2="4"></line> |
| 59 | + </svg> |
| 60 | + `, |
| 61 | + isActive: el => el.getAttribute(ALIGN_ATTR) === 'left', |
| 62 | + apply: (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 63 | + el.setAttribute(ALIGN_ATTR, 'left') |
| 64 | + alignmentHandler.left(el, toolbarButtons) |
| 65 | + }, |
| 66 | + }, |
| 67 | + [CENTER_ALIGN]: { |
| 68 | + name: CENTER_ALIGN, |
| 69 | + icon: ` |
| 70 | + <svg viewbox="0 0 18 18"> |
| 71 | + <line class="ql-stroke" x1="15" x2="3" y1="9" y2="9"></line> |
| 72 | + <line class="ql-stroke" x1="14" x2="4" y1="14" y2="14"></line> |
| 73 | + <line class="ql-stroke" x1="12" x2="6" y1="4" y2="4"></line> |
| 74 | + </svg> |
| 75 | + `, |
| 76 | + isActive: el => el.getAttribute(ALIGN_ATTR) === 'center', |
| 77 | + apply: (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 78 | + el.setAttribute(ALIGN_ATTR, 'center') |
| 79 | + alignmentHandler.center(el, toolbarButtons) |
| 80 | + }, |
| 81 | + }, |
| 82 | + [RIGHT_ALIGN]: { |
| 83 | + name: RIGHT_ALIGN, |
| 84 | + icon: ` |
| 85 | + <svg viewbox="0 0 18 18"> |
| 86 | + <line class="ql-stroke" x1="15" x2="3" y1="9" y2="9"></line> |
| 87 | + <line class="ql-stroke" x1="15" x2="5" y1="14" y2="14"></line> |
| 88 | + <line class="ql-stroke" x1="15" x2="9" y1="4" y2="4"></line> |
| 89 | + </svg> |
| 90 | + `, |
| 91 | + isActive: el => el.getAttribute(ALIGN_ATTR) === 'right', |
| 92 | + apply: (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 93 | + el.setAttribute(ALIGN_ATTR, 'right') |
| 94 | + alignmentHandler.right(el, toolbarButtons) |
| 95 | + }, |
| 96 | + }, |
| 97 | + [DOWNLOAD]: { |
| 98 | + name: DOWNLOAD, |
| 99 | + icon: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path class="ql-fill" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10z"/></svg>`, |
| 100 | + apply: (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 101 | + alignmentHandler.download(el, toolbarButtons) |
| 102 | + }, |
| 103 | + }, |
| 104 | + [COPY]: { |
| 105 | + name: COPY, |
| 106 | + icon: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path class="ql-fill" d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"/><path class="ql-fill" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z"/></svg>`, |
| 107 | + apply: (el: HTMLImageElement, toolbarButtons: ImageToolbarButtons) => { |
| 108 | + alignmentHandler.copy(el, toolbarButtons) |
| 109 | + }, |
| 110 | + }, |
| 111 | +} |
| 112 | +export class ImageToolbarButtons { |
| 113 | + buttons: Record<string, ToolButtonOption> |
| 114 | + |
| 115 | + constructor(options: ToolbarButtonOptions) { |
| 116 | + this.buttons = Object.entries(options.buttons).reduce((acc, [name, button]) => { |
| 117 | + if (isBoolean(button) && button && defaultButtons[name]) { |
| 118 | + acc[name] = defaultButtons[name] |
| 119 | + } |
| 120 | + else if (isObject(button)) { |
| 121 | + acc[button.name] = button |
| 122 | + } |
| 123 | + return acc |
| 124 | + }, {}) |
| 125 | + } |
| 126 | + |
| 127 | + getItems(): ToolButtonOption[] { |
| 128 | + return Object.keys(this.buttons).map(k => this.buttons[k]) |
| 129 | + } |
| 130 | + |
| 131 | + clear(el: HTMLElement): void { |
| 132 | + el.removeAttribute(ALIGN_ATTR) |
| 133 | + setAlignStyle(el, null, null, null) |
| 134 | + } |
| 135 | +} |
0 commit comments