diff --git a/README.md b/README.md index 4d7cb6c..4a4f45b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ If you have Vuetify `1.x` (not `2.x`), then you can find docs and demo [here](ht - the project is ready to actively develop if there is support (stars)! - the ability to create and use your own extensions - choose where the extension buttons should be displayed: in the toolbar or in the bubble menu +- support for custom image upload. You can use any method of upload through your Vue component. - Vuetify `2.x` and `1.x` support ## Installation @@ -163,7 +164,7 @@ export default { ``` -### CDN ( + + diff --git a/demo/pages/Index.vue b/demo/pages/Index.vue index f476787..623b50d 100644 --- a/demo/pages/Index.vue +++ b/demo/pages/Index.vue @@ -27,6 +27,7 @@ + + diff --git a/src/extensions/nativeExtensions/image/ImageSource.ts b/src/extensions/nativeExtensions/image/ImageSource.ts new file mode 100644 index 0000000..4744e73 --- /dev/null +++ b/src/extensions/nativeExtensions/image/ImageSource.ts @@ -0,0 +1,4 @@ +export default interface ImageSource { + src: null | string + alt: null | string +} diff --git a/src/extensions/nativeExtensions/image/ImageUploadArea.vue b/src/extensions/nativeExtensions/image/ImageUploadArea.vue index 4ad8adb..df47023 100644 --- a/src/extensions/nativeExtensions/image/ImageUploadArea.vue +++ b/src/extensions/nativeExtensions/image/ImageUploadArea.vue @@ -22,7 +22,7 @@

-

Choose a file(s) or drag it here.

+

{{ $i18n.getMsg('extensions.Image.window.imageUpload.instruction') }}

@@ -32,10 +32,8 @@ import { mixins } from 'vue-class-component' import { Component } from 'vue-property-decorator' import I18nMixin from '~/mixins/I18nMixin' +import EVENTS from '~/extensions/nativeExtensions/image/events' -export const EVENTS = { - SELECT_FILES: 'select-files' as const -} const HOLDER_CLASS = 'tiptap-vuetify-image-upload-area-holder' @Component @@ -46,7 +44,7 @@ export default class ImageUploadArea extends mixins(I18nMixin) { input.addEventListener('change', e => { if (e.target instanceof HTMLInputElement) { - this.$emit(EVENTS.SELECT_FILES, e.target.files) + this.filesSelected(e.target.files) holder.classList.remove(HOLDER_CLASS + '--dragover') e.target.value = '' @@ -66,7 +64,20 @@ export default class ImageUploadArea extends mixins(I18nMixin) { holder.addEventListener('dragend', dragleaveOrEndHandler) holder.addEventListener('drop', e => { e.preventDefault() - this.$emit(EVENTS.SELECT_FILES, e.dataTransfer!.files) + this.filesSelected(e.dataTransfer!.files) + }) + } + filesSelected (files: HTMLInputElement['files']) { + [...files].forEach(file => { + const reader = new FileReader() + reader.addEventListener('load', readerEvent => { + // TODO URL.createObjectURL(file) and upload + this.$emit(EVENTS.SELECT_FILE, { + src: readerEvent.target!.result!.toString(), + alt: file.name + }) + }) + reader.readAsDataURL(file) }) } } diff --git a/src/extensions/nativeExtensions/image/ImageWindow.vue b/src/extensions/nativeExtensions/image/ImageWindow.vue index 910a997..f47d8ff 100644 --- a/src/extensions/nativeExtensions/image/ImageWindow.vue +++ b/src/extensions/nativeExtensions/image/ImageWindow.vue @@ -32,7 +32,8 @@ cols="4" > + - - - - -
-
- {{ $i18n.getMsg('extensions.Image.window.or') }} -
- - -
-
+ + + import { mixins } from 'vue-class-component' import { Component, Prop } from 'vue-property-decorator' -import { VRow, VCol, VImg, VDialog, VCard, VCardTitle, VCardText, VCardActions, VBtn, VSpacer, VIcon, VTextField } from 'vuetify/lib' +import { VRow, VCol, VImg, VDialog, VCard, VCardTitle, VCardText, VCardActions, VBtn, VSpacer, VIcon, VTextField, VTabs, VTab, VTabsSlider, VTabItem, VTabsItems } from 'vuetify/lib' import I18nMixin from '~/mixins/I18nMixin' import ImageUploadArea from '~/extensions/nativeExtensions/image/ImageUploadArea.vue' +import ImageForm from '~/extensions/nativeExtensions/image/ImageForm.vue' +import ImageSource from '~/extensions/nativeExtensions/image/ImageSource' import { VExpandTransition } from 'vuetify/lib/components/transitions' import { COMMON_ICONS } from '~/configs/theme' @@ -99,11 +111,13 @@ export const PROPS = { VALUE: 'value' as const, CONTEXT: 'context' as const, EDITOR: 'editor' as const, + IMAGE_SOURCES: 'imageSources' as const, + IMAGE_SOURCES_OVERRIDE: 'imageSourcesOverride' as const, NATIVE_EXTENSION_NAME: 'nativeExtensionName' as const } @Component({ - components: { VRow, VCol, VExpandTransition, ImageUploadArea, VImg, VDialog, VCard, VCardTitle, VCardText, VCardActions, VBtn, VSpacer, VIcon, VTextField } + components: { VRow, VCol, VExpandTransition, ImageForm, ImageUploadArea, VImg, VDialog, VCard, VCardTitle, VCardText, VCardActions, VBtn, VSpacer, VIcon, VTextField, VTabs, VTab, VTabsSlider, VTabItem, VTabsItems } }) export default class ImageWindow extends mixins(I18nMixin) { @Prop({ @@ -130,50 +144,81 @@ export default class ImageWindow extends mixins(I18nMixin) { }) readonly [PROPS.EDITOR]: any + @Prop({ + type: Array, + required: false + }) + readonly [PROPS.IMAGE_SOURCES]: any + + @Prop({ + type: Boolean, + required: false + }) + readonly [PROPS.IMAGE_SOURCES_OVERRIDE]: any + readonly COMMON_ICONS = COMMON_ICONS - form: { - src: null | string - } = { - src: null // 'https://www.nationalgeographic.com/content/dam/news/2018/05/17/you-can-train-your-cat/02-cat-training-NationalGeographic_1484324.jpg' + readonly defaultImageTabs = [ + { + name: 'URL', + component: ImageForm + }, + { + name: 'Upload', + component: ImageUploadArea + } + ] + + inputPreviewSources: ImageSource[] = [] + + get imageTabs () { + if (this[PROPS.IMAGE_SOURCES]) { + if (this[PROPS.IMAGE_SOURCES_OVERRIDE]) { + return this[PROPS.IMAGE_SOURCES] + } + return this.defaultImageTabs.concat(this[PROPS.IMAGE_SOURCES]) + } + return this.defaultImageTabs } - inputPreviewSources: string[] = [] get previewSources () { - return [this.form.src, ...this.inputPreviewSources].filter(Boolean) + return this.inputPreviewSources.filter(Boolean) } get isDisabled () { return !this.previewSources.length } - removeSource (source) { + removeSource (source: ImageSource) { if (this.inputPreviewSources.includes(source)) { this.inputPreviewSources = this.inputPreviewSources.filter(i => i !== source) - } else if (this.form.src === source) { - this.form.src = null } } - onFilesSelect (files: HTMLInputElement['files']) { - [...files].forEach(file => { - const reader = new FileReader() + onFileSelect (file: ImageSource) { + if (file.src !== null && file.src !== '') { + const existingFile = this.findFile(file) + if (existingFile !== null) { + existingFile.alt = file.alt + } else { + this.inputPreviewSources.push(file) + } + } + } - reader.addEventListener('load', readerEvent => { - // TODO URL.createObjectURL(file) and upload - this.inputPreviewSources.push(readerEvent.target!.result!.toString()) - }) - reader.readAsDataURL(file) + findFile (file: ImageSource) { + const matches: ImageSource[] = this.inputPreviewSources.filter((source: ImageSource) => { + return (source.src === file.src) }) + if (matches.length > 0) { + return matches[0] + } + return null } apply () { this.previewSources.forEach(src => { - this[PROPS.CONTEXT].commands[this[PROPS.NATIVE_EXTENSION_NAME]]({ - // TODO alt, title - src, - alt: 'Image' - }) + this[PROPS.CONTEXT].commands[this[PROPS.NATIVE_EXTENSION_NAME]](src) }) this.close() diff --git a/src/extensions/nativeExtensions/image/events.ts b/src/extensions/nativeExtensions/image/events.ts new file mode 100644 index 0000000..8907653 --- /dev/null +++ b/src/extensions/nativeExtensions/image/events.ts @@ -0,0 +1,3 @@ +export default { + SELECT_FILE: 'select-file' as const +} diff --git a/src/i18n/de/index.ts b/src/i18n/de/index.ts index 967b560..2dad4fd 100644 --- a/src/i18n/de/index.ts +++ b/src/i18n/de/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: 'Bild hinzufügen', - or: 'ODER', form: { sourceLink: 'Bild URL' }, diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 160ed66..595fcd6 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -121,9 +121,13 @@ export default { }, window: { title: 'Add Image', - or: 'OR', form: { - sourceLink: 'Image URL' + sourceLink: 'Image URL', + altText: 'Alternative Text', + addImage: 'Add Image' + }, + imageUpload: { + instruction: 'Choose a file(s) or drag it here.' }, buttons: { close: 'Close', diff --git a/src/i18n/es/index.ts b/src/i18n/es/index.ts index a6b375b..b40f3e7 100644 --- a/src/i18n/es/index.ts +++ b/src/i18n/es/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: 'Agregar Imagen', - or: 'O', form: { sourceLink: 'URL de la Imagen' }, diff --git a/src/i18n/index.ts b/src/i18n/index.ts index f12834d..fd1db5b 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -56,6 +56,10 @@ export function getMsg (path: string, args?, lang: null | string = null): string target = path.split('.').reduce((prev: string, curr: string) => { return prev[curr] }, dictionaryByLang) + // No error thrown by above reduce function if last stage is undefined - no fallback used and returned value is empty + if (target === undefined) { + throw new Error(`${path} is undefined.`) + } } catch (e) { ConsoleLogger.warn(`Cannot get translation "${path}" for language "${currentLang}". Fallback "${defaultLanguage}" is used instead. Contribution to github is welcome.`) diff --git a/src/i18n/ja/index.ts b/src/i18n/ja/index.ts index 9507468..dfc935f 100644 --- a/src/i18n/ja/index.ts +++ b/src/i18n/ja/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: '画像を追加する', - or: 'または', form: { sourceLink: '画像URL' }, diff --git a/src/i18n/ko/index.ts b/src/i18n/ko/index.ts index c78946d..252f245 100644 --- a/src/i18n/ko/index.ts +++ b/src/i18n/ko/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: '이미지 추가', - or: '또는', form: { sourceLink: '이미지 URL' }, diff --git a/src/i18n/nl/index.ts b/src/i18n/nl/index.ts index e69547c..78b5de6 100644 --- a/src/i18n/nl/index.ts +++ b/src/i18n/nl/index.ts @@ -121,9 +121,12 @@ export default { }, window: { title: 'Afbeelding toevoegen', - or: 'OF', form: { - sourceLink: 'Afbeelding URL' + sourceLink: 'Afbeelding URL', + altText: 'Alternatieve Tekst' + }, + imageUpload: { + instruction: 'Kies een of meerdere bestanden of sleep ze hiernaartoe.' }, buttons: { close: 'Sluiten', diff --git a/src/i18n/pl/index.ts b/src/i18n/pl/index.ts index a6c2d1d..1ef8e1c 100644 --- a/src/i18n/pl/index.ts +++ b/src/i18n/pl/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: 'Dodaj obrazek', - or: 'LUB', form: { sourceLink: 'Adres URL obrazka' }, diff --git a/src/i18n/ru/index.ts b/src/i18n/ru/index.ts index 142d42e..9ff0501 100644 --- a/src/i18n/ru/index.ts +++ b/src/i18n/ru/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: 'Добавить картинку', - or: 'ИЛИ', form: { sourceLink: 'Ссылка на картинку' }, diff --git a/src/i18n/zh/index.ts b/src/i18n/zh/index.ts index 1663c64..bbd898b 100644 --- a/src/i18n/zh/index.ts +++ b/src/i18n/zh/index.ts @@ -121,7 +121,6 @@ export default { }, window: { title: '添加图片', - or: '或者', form: { sourceLink: '图片链接' },