diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bcfec6bb..b1c0d0af 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,14 +9,35 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: python-version: 3.9 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install Node.js Dependencies + run: yarn + + - name: Build Widget JavaScript + run: yarn gulp js:widget + + - name: Build Inline JavaScript + run: yarn gulp js:inline + + - name: Build SCSS + run: yarn gulp scss + - name: Install pypa/build run: python -m pip install build --user + - name: Build a binary wheel and a source tarball run: python -m build --sdist --wheel --outdir dist/ + - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@master with: diff --git a/gulpfile.js b/gulpfile.js index 9a488e21..26f74741 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -14,6 +14,9 @@ const version = require('./package.json').version; const JS_WIDGET_INPUT = './src/ImageUploaderWidget.ts'; const JS_WIDGET_NAME = 'image-uploader.js'; const JS_WIDGET_NAME_MIN = 'image-uploader.min.js'; +const JS_INLINE_INPUT = './src/ImageUploaderInline.ts'; +const JS_INLINE_NAME = 'image-uploader-inline.js'; +const JS_INLINE_NAME_MIN = 'image-uploader-inline.min.js'; const JS_OUTPUT = './image_uploader_widget/static/admin/js'; const SCSS_NAME = 'image-uploader.css'; const SCSS_NAME_MIN = 'image-uploader.min.css'; @@ -36,7 +39,7 @@ function onError(err) { this.emit('end'); } -gulp.task('js', (callback) => { +gulp.task('js:widget', (callback) => { pump([ gulp.src(JS_WIDGET_INPUT), babel({ @@ -56,6 +59,26 @@ gulp.task('js', (callback) => { ], callback); }); +gulp.task('js:inline', (callback) => { + pump([ + gulp.src(JS_INLINE_INPUT), + babel({ + presets: ['@babel/preset-env', '@babel/preset-typescript'], + plugins: [ + 'babel-plugin-remove-import-export', + '@babel/plugin-proposal-class-properties', + ] + }), + rename(JS_INLINE_NAME), + header(HEADER), + gulp.dest(JS_OUTPUT), + uglify().on('error', onError), + rename(JS_INLINE_NAME_MIN), + header(HEADER), + gulp.dest(JS_OUTPUT), + ], callback); +}); + gulp.task('scss-expanded', (callback) => { pump([ gulp.src(SCSS_INPUT), diff --git a/image_uploader_widget/admin.py b/image_uploader_widget/admin.py index d13f0748..95b4bac2 100644 --- a/image_uploader_widget/admin.py +++ b/image_uploader_widget/admin.py @@ -9,10 +9,9 @@ class ImageUploaderInline(admin.StackedInline): @property def media(self): extra = '' if settings.DEBUG else '.min' - js = ['vendor/jquery/jquery%s.js' % extra, 'jquery.init.js', 'image_uploader_inline.js'] return forms.Media( js = [ - 'admin/js/%s' % url for url in js + 'admin/js/image-uploader-inline%s.js' % extra ], css = { 'screen': [ diff --git a/image_uploader_widget/locale/pt_BR/LC_MESSAGES/django.mo b/image_uploader_widget/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 00000000..00701312 Binary files /dev/null and b/image_uploader_widget/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/image_uploader_widget/locale/pt_BR/LC_MESSAGES/django.po b/image_uploader_widget/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 00000000..e2142d71 --- /dev/null +++ b/image_uploader_widget/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,24 @@ +msgid "" +msgstr "" +"Project-Id-Version: image_uploader_widget\n" +"Report-Msgid-Bugs-To: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgid "Drop your image here or click to select one..." +msgstr "Solte sua imagem aqui ou clique para selecionar uma..." + +msgid "Drop your image here..." +msgstr "Solte sua imagem aqui..." + +msgid "Drop your images here or click to select..." +msgstr "Solte suas imagens aqui ou clique para selecionar..." + +msgid "Drop your images here..." +msgstr "Solte suas imagens aqui..." + +msgid "Add image" +msgstr "Adicionar Imagem" diff --git a/image_uploader_widget/static/admin/js/image_uploader_inline.js b/image_uploader_widget/static/admin/js/image_uploader_inline.js deleted file mode 100644 index 21f42807..00000000 --- a/image_uploader_widget/static/admin/js/image_uploader_inline.js +++ /dev/null @@ -1,203 +0,0 @@ -{ - 'use strict'; - $ = window.django.jQuery; - $(function(){ - $.fn.inlineImageUploader = function() { - const handler = { - /** - * The initiated elements collection. - */ - elements: $(this), - /** - * Update the indexes in a item. - * @param {HTMLElement} el The element to update indexes. - * @param {String} prefix The item prefix. - * @param {Number} ndx The item index. - */ - updateElementIndex: function(el, prefix, ndx) { - const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); - const replacement = prefix + "-" + ndx; - if ($(el).prop("for")) { - $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); - } - if (el.id) { - id = el.id.replace(id_regex, replacement) - el.id = id; - } - if (el.name) { - el.name = el.name.replace(id_regex, replacement); - } - }, - /** - * Update all item indexes in a root element. - * @param {HTMLElement} root The root html element. - */ - updateAllIndexes: function(root) { - var items = root.find('.inline-related:not(.empty-form)'); - var prefix = root.data('prefix'); - var i; - for (i = 0; i < items.length; i += 1) { - this.updateElementIndex($(items).get(i), prefix, i); - var that = this; - $(items.get(i)).find("*").each(function(){ - that.updateElementIndex($(this).get(0), prefix, i); - }); - } - root.data('next', i); - var totalForms = root.find('#id_' + prefix + '-TOTAL_FORMS'); - totalForms.val(i); - var maxForms = root.find('#id_' + prefix + '-MAX_NUM_FORMS'); - if ((maxForms.val() === '') || (maxForms.val() - i) > 0) { - root.find('.iuw-add-image-btn').addClass('visible-by-number'); - } else { - root.find('.iuw-add-image-btn').removeClass('visible-by-number'); - } - }, - /** - * Check if we have any item in the root and marker the root with a class. - * @param {HTMLElement} root The root element to check if we have any item. - */ - updateEmpty: function(root) { - var childs = $(root).find('.inline-related:not(.empty-form):not(.deleted)'); - if (childs.length > 0) { - root.addClass('non-empty'); - } else { - root.removeClass('non-empty'); - } - }, - /** - * Item click event Handler. - * @param {Event} e The event object. - */ - callFileClick: function(e) { - var root = $(this).closest('.iuw-inline-root'); - var data = root.data('iuw'); - var item = $(this).closest('.inline-related'); - if ($(e.target).hasClass('iuw-delete-icon')) { - if (item.attr('data-raw')) { - item.addClass('deleted'); - item.find('input[type=checkbox]').prop('checked', true); - } else { - item.remove(); - } - data.updateEmpty(root); - return; - } - var file = item.find('input[type=file]'); - if (e.target == file[0]) { - return; - } - file.trigger('click'); - }, - /** - * Inner file input change event. - * @param {Event} e The event object. - */ - fileInputChange: function(e) { - var files = $(this).prop('files'); - if (files.length <= 0) { - return; - } - var blob = URL.createObjectURL(files[0]); - $(this).closest('.inline-related').find('img').attr('src', blob); - }, - /** - * Append a '.inline-related' markup to the '.inline-related'. - * @param {HTMLElement} el The element to append the '.inline-related' inner markup. - * @param {String} url The image url of the item. - */ - appendItem: function(el, url) { - var delete_icon = ''; - var item = $(el).closest('.inline-related'); - if (item.data('candelete')) { - delete_icon = 'X' - } - $(el).append( - '' + delete_icon - ); - item.off('click'); - item.on('click', this.callFileClick); - var fileInput = item.find('input[type=file]'); - fileInput.off('change'); - fileInput.on('change', this.fileInputChange); - }, - /** - * Adjust inline related element to this script standards. - * @param {HTMLElement} element The '.inline-related' element. - */ - adjustInlineRelated: function(element) { - var hiddenInputs = $(element).find('input[type=hidden]'); - var rawImage = $(element).find('p.file-upload a'); - var fileInput = $(element).find('input[type=file]'); - var checkBoxInput = $(element).find('input[type=checkbox]'); - if (rawImage) { - $(element).attr('data-raw', rawImage.attr('href')); - } - checkBoxInput.remove(); - hiddenInputs.remove(); - fileInput.remove(); - $(element).html(''); - $(element).append(hiddenInputs); - $(element).append(fileInput); - $(element).append(checkBoxInput); - if ($(element).attr('data-raw')) { - this.appendItem($(element), $(element).attr('data-raw')); - } - }, - /** - * Add image event handler. - */ - handleAddImage: function(){ - var root = $(this).closest('.iuw-inline-root'); - var iuw = root.data('iuw'); - if (root.find('input[type=file].temp_file').length == 0) { - root.append(''); - root.find('input[type=file].temp_file').on('change', function(e){ - var fileList = $(this).prop('files'); - $(this).off('change'); - $(this).remove(); - var template = root.find('.inline-related.empty-form'); - var row = template.clone(true) - .removeClass('empty-form') - .removeClass('last-related') - .attr('data-candelete', true) - .attr("id", root.data('prefix') + "-" + root.data('next')); - $(row).insertBefore($(template)); - row.find('input[type=file]').prop('files', fileList); - var blob = URL.createObjectURL(fileList[0]); - iuw.appendItem(row, blob); - iuw.updateEmpty(root); - iuw.updateAllIndexes(root); - }); - } - root.find('input[type=file].temp_file').trigger('click'); - }, - /** - * Initialize the image uploader inline. - */ - init: function() { - const that = this; - this.elements.each(function(index, element){ - var data = $(element).closest('.inline-group').data(); - $(element).data('prefix', data.inlineFormset.options.prefix); - that.updateEmpty($(element)); - that.updateAllIndexes($(element)); - - $(element).find('.inline-related').each(function(index, related){ - that.adjustInlineRelated(related); - }); - $(element).find('.iuw-add-image-btn').on('click', that.handleAddImage); - $(element).find('.iuw-empty').on('click', that.handleAddImage); - $(element).data('iuw', that); - }); - } - }; - handler.init(); - return handler; - }; - - $(document).ready(function(){ - $('.iuw-inline-root').inlineImageUploader(); - }); - }); -} diff --git a/image_uploader_widget/templates/admin/edit_inline/image_uploader.html b/image_uploader_widget/templates/admin/edit_inline/image_uploader.html index 6b89a89c..674455c7 100644 --- a/image_uploader_widget/templates/admin/edit_inline/image_uploader.html +++ b/image_uploader_widget/templates/admin/edit_inline/image_uploader.html @@ -1,4 +1,5 @@ {% load i18n admin_urls static %} +
{% endfor %}
- - - Add + + {% translate 'Add image' %}
- - Click here to select a file! + + {% translate 'Drop your images here or click to select...' %} +
+
+ + {% translate 'Drop your images here...' %}
diff --git a/image_uploader_widget/templates/admin/widgets/image_uploader_widget.html b/image_uploader_widget/templates/admin/widgets/image_uploader_widget.html new file mode 100644 index 00000000..80ab5885 --- /dev/null +++ b/image_uploader_widget/templates/admin/widgets/image_uploader_widget.html @@ -0,0 +1,17 @@ +{% load i18n %} +
+ + +
+ + {% translate 'Drop your image here...' %} +
+
+ + {% translate 'Drop your image here or click to select one...' %} +
+ + {% if not widget.required %} + + {% endif %} +
diff --git a/image_uploader_widget/templates/widgets/image_uploader_widget.html b/image_uploader_widget/templates/widgets/image_uploader_widget.html deleted file mode 100644 index 805da1a5..00000000 --- a/image_uploader_widget/templates/widgets/image_uploader_widget.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - -
- Drop your file here. -
- -
- {{ widget.non_file_text }} -
- - {% if not widget.required %} - - {% endif %} -
diff --git a/image_uploader_widget/widgets.py b/image_uploader_widget/widgets.py index 40d970c5..90803b14 100644 --- a/image_uploader_widget/widgets.py +++ b/image_uploader_widget/widgets.py @@ -3,7 +3,7 @@ from django.conf import settings class ImageUploaderWidget(widgets.ClearableFileInput): - template_name = 'widgets/image_uploader_widget.html' + template_name = 'admin/widgets/image_uploader_widget.html' non_file_text = '' def __init__(self, non_file_text = 'Click here to select a file!', attrs = None): diff --git a/image_uploader_widget_demo/settings.py b/image_uploader_widget_demo/settings.py index df085593..8ee07f50 100644 --- a/image_uploader_widget_demo/settings.py +++ b/image_uploader_widget_demo/settings.py @@ -76,7 +76,7 @@ # Internationalization # https://docs.djangoproject.com/en/1.6/topics/i18n/ -LANGUAGE_CODE = "en-us" +LANGUAGE_CODE = "pt-BR" TIME_ZONE = "UTC" diff --git a/package.json b/package.json index 0b698242..f3c49d8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "django-image-uploader-widget", - "version": "0.0.7", + "version": "0.1.0", "main": "index.js", "repository": "https://github.com/EduardoJM/django-image-uploader-widget.git", "author": "Eduardo Oliveira ", diff --git a/setup.py b/setup.py index 50496ee6..89cf816e 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='django-image-uploader-widget', - version='0.0.6', + version='0.1.0', description='Simple Image Uploader Widget for Django-Admin', long_description=readme, long_description_content_type='text/markdown', diff --git a/src/ImageUploaderInline.ts b/src/ImageUploaderInline.ts new file mode 100644 index 00000000..ebb0d6d9 --- /dev/null +++ b/src/ImageUploaderInline.ts @@ -0,0 +1,276 @@ +interface ImageUploaderInlineFormSet { + name: string; + options: { + prefix: string; + addText: string; + deleteText: string; + } +} + +class ImageUploaderInline { + element: HTMLElement; + inlineGroup: HTMLElement; + inlineFormset: ImageUploaderInlineFormSet; + tempFileInput: HTMLInputElement | null = null; + next: number = 0; + dragging: boolean = false; + + constructor(element: HTMLElement) { + this.element = element; + this.inlineGroup = element.closest('.inline-group'); + this.inlineFormset = JSON.parse( + this.inlineGroup.getAttribute('data-inline-formset'), + ); + + this.updateEmpty(); + this.updateAllIndexes(); + + Array + .from(this.element.querySelectorAll('.inline-related')) + .forEach((item) => this.adjustInlineRelated(item)); + Array + .from(this.element.querySelectorAll('.iuw-add-image-btn, .iuw-empty')) + .forEach((item) => item.addEventListener('click', this.onChooseAddImageAreaClick)); + + this.element.addEventListener('dragenter', this.onDragEnter); + this.element.addEventListener('dragover', this.onDragOver); + this.element.addEventListener('dragleave', this.onDragLeave); + this.element.addEventListener('dragend', this.onDragLeave); + this.element.addEventListener('drop', this.onDrop); + } + + onDrop = (e: DragEvent) => { + e.preventDefault(); + + this.dragging = false; + this.element.classList.remove('drop-zone'); + + if (e.dataTransfer.files.length) { + for (const file of e.dataTransfer.files) { + this.addFile(file); + } + } + } + + onDragEnter = () => { + this.dragging = true; + this.element.classList.add('drop-zone'); + } + + onDragOver = (e: DragEvent) => { + if (e) { + e.preventDefault(); + } + } + + onDragLeave = (e: DragEvent) => { + if (e.relatedTarget && (e.relatedTarget as HTMLElement).closest('.iuw-inline-root') === this.element) { + return; + } + this.dragging = false; + this.element.classList.remove('drop-zone'); + } + + updateEmpty() { + const { length } = this.element.querySelectorAll('.inline-related:not(.empty-form):not(.deleted)'); + if (length > 0) { + this.element.classList.add('non-empty'); + } else { + this.element.classList.remove('non-empty'); + } + } + + updateElementIndex(element: HTMLElement, prefix: string, index: number) { + const id_regex = new RegExp(`(${prefix}-(\\d+|__prefix__))`); + const replacement = `${prefix}-${index}`; + if (element.getAttribute('for')) { + element.setAttribute('for', element.getAttribute('for').replace(id_regex, replacement)); + } + if (element.id) { + element.id = element.id.replace(id_regex, replacement); + } + if (element.getAttribute('name')) { + element.setAttribute('name', element.getAttribute('name').replace(id_regex, replacement)); + } + } + + updateAllIndexes() { + const { prefix } = this.inlineFormset.options; + const { length: count } = Array + .from(this.element.querySelectorAll('.inline-related:not(.empty-form)')) + .map((item) => item as HTMLElement) + .map((item, index) => { + this.updateElementIndex(item, prefix, index); + Array + .from(item.querySelectorAll('*')) + .map((childItem) => childItem as HTMLElement) + .forEach((childItem) => { + this.updateElementIndex(childItem, prefix, index); + }); + return item; + }); + this.next = count; + const totalFormsInput = document.getElementById(`id_${prefix}-TOTAL_FORMS`) as HTMLInputElement; + totalFormsInput.value = String(this.next); + const maxFormsInput = document.getElementById(`id_${prefix}-MAX_NUM_FORMS`) as HTMLInputElement; + let maxNumber = parseInt(maxFormsInput.value, 10); + if (Number.isNaN(maxNumber)) { + maxNumber = 0; + } + if (maxFormsInput.value === '' || maxNumber - this.next > 0) { + this.element + .querySelector('.iuw-add-image-btn') + .classList.add('visible-by-number'); + } else { + this.element + .querySelector('.iuw-add-image-btn') + .classList.remove('visible-by-number'); + } + } + + adjustInlineRelated(element: Element) { + const inputs = Array + .from( + element.querySelectorAll('input[type=hidden], input[type=checkbox], input[type=file]'), + ) + .map((item) => { + item.parentElement.removeChild(item); + return item; + }); + // get raw image url + let rawImage = document.querySelector('p.file-upload a'); + if (element.classList.contains('empty-form')) { + rawImage = null; + } + if (rawImage) { + element.setAttribute('data-raw', rawImage.getAttribute('href')); + } + // clear element + element.innerHTML = ''; + inputs.forEach((item) => element.appendChild(item)); + // apply raw image + if (rawImage) { + this.appendItem(element, rawImage.getAttribute('href')); + } + } + + onRelatedItemClick = (e: Event) => { + if (!e || !e.target) { + return; + } + const target = e.target as HTMLElement; + const item = target.closest('.inline-related'); + if (target.closest('.iuw-delete-icon')) { + if (item.getAttribute('data-raw')) { + item.classList.add('deleted'); + const checkboxInput = item.querySelector('input[type=checkbox]') as HTMLInputElement; + checkboxInput.checked = true; + } else { + item.parentElement.removeChild(item); + } + this.updateEmpty(); + return; + } + var fileInput = item.querySelector('input[type=file]') as HTMLInputElement; + if (e.target === fileInput) { + return; + } + fileInput.click(); + } + + onFileInputChange = (e: Event) => { + const target = e.target as HTMLElement; + if (target.tagName !== 'INPUT') { + return; + } + const fileInput = target as HTMLInputElement; + var files = fileInput.files; + if (files.length <= 0) { + return; + } + const imgTag = target.closest('.inline-related').querySelector('img'); + if (imgTag) { + imgTag.src = URL.createObjectURL(files[0]); + } + } + + appendItem(element: Element, url: string) { + let delete_icon: Element | null = null; + const related = element.closest('.inline-related'); + if (related.getAttribute('data-candelete') === 'true') { + delete_icon = document.createElement('span'); + delete_icon.classList.add('iuw-delete-icon'); + delete_icon.innerHTML = ''; + } + const img = document.createElement('img'); + img.src = url; + element.appendChild(img); + if (delete_icon) { + element.appendChild(delete_icon); + } + related.removeEventListener('click', this.onRelatedItemClick); + related.addEventListener('click', this.onRelatedItemClick); + const fileInput = related.querySelector('input[type=file]'); + fileInput.removeEventListener('change', this.onFileInputChange); + fileInput.addEventListener('change', this.onFileInputChange); + } + + onTempFileChange = () => { + const filesList = this.tempFileInput.files; + if (filesList.length <= 0) { + return; + } + + this.tempFileInput.removeEventListener('change', this.onTempFileChange); + this.tempFileInput.parentElement.removeChild(this.tempFileInput); + this.tempFileInput = null; + + this.addFile(filesList[0]); + } + + addFile(file: File) { + const template = this.element.querySelector('.inline-related.empty-form'); + if (!template) { + return; + } + const row = template.cloneNode(true) as HTMLElement; + row.classList.remove('empty-form'); + row.classList.remove('last-related'); + row.setAttribute('data-candelete', 'true'); + row.id = `${this.inlineFormset.options.prefix}-${this.next}`; + + template.parentElement.insertBefore(row, template); + + const dataTransferList = new DataTransfer(); + dataTransferList.items.add(file); + + const rowFileInput = row.querySelector('input[type=file]') as HTMLInputElement; + rowFileInput.files = dataTransferList.files; + + this.appendItem(row, URL.createObjectURL(file)); + this.updateEmpty(); + this.updateAllIndexes(); + } + + onChooseAddImageAreaClick = () => { + if (!this.tempFileInput) { + this.tempFileInput = document.createElement('input'); + this.tempFileInput.setAttribute('type', 'file'); + this.tempFileInput.classList.add('temp_file'); + this.tempFileInput.setAttribute('accept', 'image/*'); + this.tempFileInput.style.display = 'none'; + this.tempFileInput.addEventListener('change', this.onTempFileChange); + this.element.appendChild(this.tempFileInput); + } + this.tempFileInput.click(); + } +} + +document.addEventListener('DOMContentLoaded', () => { + Array + .from(document.querySelectorAll('.iuw-inline-root')) + .map((element) => new ImageUploaderInline(element as HTMLElement)); +}); + +// export for testing +export { ImageUploaderInline }; diff --git a/src/ImageUploaderWidget.scss b/src/ImageUploaderWidget.scss index 59ca40b8..478df29b 100644 --- a/src/ImageUploaderWidget.scss +++ b/src/ImageUploaderWidget.scss @@ -1,202 +1,4 @@ -body { - --iuw-background: #ffffff; - --iuw-color: #222; - --iuw-border-color: #222; - --iuw-item-background-color: #CCC; - --iuw-item-foreground-color: #222; - - @media (prefers-color-scheme: dark) { - --iuw-background: #121212; - --iuw-color: #FFF; - --iuw-border-color: #CCC; - --iuw-item-background-color: #CCC; - --iuw-item-foreground-color: #121212; - } - - @mixin widget-root { - user-select: none; - - min-width: 300px; - height: 200px; - - border-radius: 5px; - padding: 5px; - - background-color: var(--iuw-background); - border: 1px solid var(--iuw-border-color); - color: var(--iuw-color); - - position: relative; - - overflow-y: hidden; - overflow-x: auto; - - display: flex; - flex-direction: row; - align-items: stretch; - - input[type=file], - input[type=checkbox] { - display: none; - } - - .iuw-empty { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - - flex-direction: column; - align-items: center; - justify-content: center; - - cursor: pointer; - - display: none; - } - &:not(.non-empty) { - .iuw-empty { - display: flex; - } - } - &.drop-zone { - .iuw-empty { - display: none; - } - } - - .iuw-delete-icon { - width: 32px; - height: 32px; - background-color: rgba(0, 0, 0, 0.3); - border-radius: 50%; - - position: absolute; - top: 0; - right: 0; - z-index: 50; - - display: flex; - align-items: center; - justify-content: center; - } - - .iuw-drop-label { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - - background: red; - - display: none; - align-items: center; - justify-content: center; - text-align: center; - - z-index: 50; - } - - &.drop-zone { - background: blue; - - .iuw-drop-label { - display: flex; - } - } - } - - @mixin image-preview { - display: flex; - align-items: center; - justify-content: center; - - width: 160px; - margin: 0 5px; - - border-radius: 5px; - padding: 5px; - - background: var(--iuw-item-background-color); - - cursor: pointer; - - position: relative; - - img { - max-width: 100%; - max-height: 100%; - } - } - - .iuw-root { - @include widget-root(); - - .iuw-image-preview { - @include image-preview(); - } - } - - .iuw-inline-root { - @include widget-root(); - - .inline-related { - @include image-preview(); - - &.empty-form { - display: none; - } - &.deleted { - display: none; - } - } - - > div { - height: 100%; - width: auto; - - display: flex; - flex-direction: row; - align-items: stretch; - } - - .iuw-add-image-btn { - width: 160px; - background: var(--iuw-item-background-color); - color: var(--iuw-item-foreground-color); - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - border-radius: 5px; - padding: 5px; - - cursor: pointer; - - display: none; - - svg { - fill: var(--iuw-item-foreground-color); - } - } - - &.non-empty { - &:not(.visible-by-number) { - display: none; - } - - &.visible-by-number { - display: flex; - } - } - &:not(.non-empty) { - .iuw-add-image-btn { - display: none; - } - } - } -} +@import "./styles/variables"; +@import "./styles/mixins"; +@import "./styles/widget"; +@import "./styles/inline"; diff --git a/src/ImageUploaderWidget.test.ts b/src/ImageUploaderWidget.test.ts index 40ee0b59..3153c67b 100644 --- a/src/ImageUploaderWidget.test.ts +++ b/src/ImageUploaderWidget.test.ts @@ -94,3 +94,4 @@ test('[Required Widget] must be possible to upload file and widget must be rende expect(img).not.toBeNull(); expect(img.src).toBe('test::/file.png'); }); + diff --git a/src/ImageUploaderWidget.ts b/src/ImageUploaderWidget.ts index 319311d6..17385750 100644 --- a/src/ImageUploaderWidget.ts +++ b/src/ImageUploaderWidget.ts @@ -110,7 +110,7 @@ class ImageUploaderWidget { if (this.canDelete) { const span = document.createElement('span'); span.classList.add('iuw-delete-icon'); - span.innerHTML = 'X'; + span.innerHTML = ''; preview.appendChild(span); } return preview; diff --git a/src/styles/_inline.scss b/src/styles/_inline.scss new file mode 100644 index 00000000..8205fcea --- /dev/null +++ b/src/styles/_inline.scss @@ -0,0 +1,117 @@ +.iuw-inline-root, .iuw-inline-root * { + box-sizing: border-box; +} + +.iuw-inline-root { + /* base widget */ + @include make-root(); + + /* empty label */ + .iuw-empty { + @include make-empty(); + } + &:not(.non-empty) { + .iuw-empty { + height: 100%; + opacity: 1; + } + } + &.drop-zone { + .iuw-empty { + height: 0; + opacity: 0; + } + } + + /* drop label */ + .iuw-drop-label { + @include make-drop-label(); + } + &.drop-zone { + .iuw-drop-label { + height: 100%; + opacity: 1; + } + } + + /* image preview */ + .inline-related { + @include make-image-preview(); + + &.empty-form { + display: none; + } + &.deleted { + display: none; + } + } + + /* images carousel */ + > div { + height: 100%; + width: auto; + + @include flex-row-stretch(); + } + + /* add button */ + .iuw-add-image-btn { + /* shape */ + border-radius: 5px; + padding: 15px; + width: 160px; + max-width: 160px; + + /* styles */ + border: 1px solid var(--iuw-image-preview-border); + box-shadow: 0 0 4px 0 var(--iuw-image-preview-shadow); + background: var(--iuw-add-image-background); + color: var(--iuw-add-image-color); + + /* display */ + @include flex-column-center-center(); + display: none; + + /* behaviour */ + cursor: pointer; + + svg { + fill: var(--iuw-add-image-color); + margin-bottom: 30px; + width: 60px; + height: auto; + transition: margin 0.3s ease, width 0.3s ease, height 0.3s ease; + } + + &:hover { + svg { + margin-bottom: 10px; + width: 80px; + height: auto; + } + } + + > span { + font-weight: bold; + text-align: center; + font-size: 1rem; + } + } + + &.non-empty { + .iuw-add-image-btn { + &:not(.visible-by-number) { + display: none; + } + + &.visible-by-number { + display: flex; + } + } + } + &:not(.non-empty) { + .iuw-add-image-btn { + display: none; + } + } +} \ No newline at end of file diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss new file mode 100644 index 00000000..5c3f2ac3 --- /dev/null +++ b/src/styles/_mixins.scss @@ -0,0 +1,251 @@ +@mixin flex-row { + display: flex; + flex-direction: row; +} + +@mixin flex-column { + display: flex; + flex-direction: column; +} + +@mixin flex-column-center { + @include flex-column(); + align-items: center; +} + +@mixin flex-column-center-center { + @include flex-column-center(); + justify-content: center; +} + +@mixin flex-row-stretch { + @include flex-row(); + align-items: stretch; +} + +@keyframes arrow-flashing { + from { + opacity: 0; + transform: scale(0) translateY(12px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@mixin make-root { + /* behaviour */ + user-select: none; + + /* sizing */ + min-width: 300px; + height: 200px; + + /* shape */ + border-radius: 5px; + padding: 5px; + + /* styles */ + background-color: var(--iuw-background); + border: 3px dashed var(--iuw-border-color); + color: var(--iuw-color); + + /* positioning */ + position: relative; + + /* overflowing */ + overflow-y: hidden; + overflow-x: auto; + + /* childs */ + @include flex-row-stretch(); + + input[type=file], + input[type=checkbox] { + display: none; + } +} + +@mixin make-empty { + /* positioning */ + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 50; + + /* display */ + @include flex-column-center-center; + + /* text */ + text-align: center; + font-size: 1.3em; + font-weight: bold; + letter-spacing: 0.05em; + color: var(--iuw-placeholder-text-color); + + /* behaviour */ + cursor: pointer; + + /* animations */ + height: 0; + opacity: 0; + overflow: hidden; + transition: opacity 0.3s ease, height 0.3s ease; + + /* childs */ + > svg { + width: 50px; + height: 50px; + margin-bottom: 30px; + transition: width 0.3s ease, height 0.3s ease, margin 0.3s ease; + } + + &:hover { + > svg { + width: 80px; + height: 80px; + margin-bottom: 10px; + } + } + + > span { + text-align: center; + + span { + color: var(--iuw-placeholder-destak-color); + } + } +} + +@mixin make-drop-label { + /* positioning */ + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 55; + + /* style */ + background: var(--iuw-dropzone-background); + + /* display */ + @include flex-column-center-center(); + + /* text */ + text-align: center; + font-size: 1.3em; + font-weight: bold; + letter-spacing: 0.05em; + color: var(--iuw-placeholder-text-color); + + /* behaviour */ + cursor: grabbing; + + /* animations */ + height: 0; + opacity: 0; + overflow: hidden; + transition: opacity 0.3s ease, height 0.3s ease; + + /* childs */ + > svg { + width: 90px; + height: 90px; + margin-bottom: 20px; + transition: width 0.3s ease, height 0.3s ease, margin 0.3s ease; + + path:last-child { + transform-origin: 50% 100%; + animation: arrow-flashing 1.1s; + animation-timing-function: ease-in; + animation-fill-mode: both; + animation-iteration-count: infinite; + animation-delay: 0.3s; + } + } + + > span { + text-align: center; + + span { + color: var(--iuw-placeholder-destak-color); + } + } +} + +@mixin make-image-preview { + /* style */ + border: 1px solid var(--iuw-image-preview-border); + box-shadow: 0 0 4px 0 var(--iuw-image-preview-shadow); + + /* shape */ + width: 160px; + margin: 0 5px; + border-radius: 5px; + overflow: hidden; + + /* behaviour */ + cursor: pointer; + + /* positioning */ + position: relative; + + /* childs */ + img { + /* sizing */ + width: 100%; + height: 100%; + + /* display mode */ + object-fit: cover; + object-position: center; + } + + .iuw-delete-icon { + /* shape */ + width: 32px; + height: 32px; + border-radius: 0 5px 0 0; + + /* styles */ + border: 1px solid #BFBFBF; + border-top: none; + border-right: none; + background-color: #F5F5F5; + opacity: 0.6; + + /* positioning */ + position: absolute; + right: 0; + top: 0; + z-index: 45; + + /* display */ + @include flex-column-center-center(); + + /* animations */ + transition: opacity 0.3s ease; + + /* icon */ + svg { + width: 16px; + height: auto; + transform: none; + transition: transform 0.3s ease; + } + } + + &:hover { + .iuw-delete-icon { + opacity: 1; + + svg { + transform: scale(1.3); + } + } + } +} diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss new file mode 100644 index 00000000..a59f3dcc --- /dev/null +++ b/src/styles/_variables.scss @@ -0,0 +1,16 @@ +body { + --iuw-background: #FFF; + --iuw-border-color: #CCC; + --iuw-color: #333; + --iuw-placeholder-text-color: #AAA; + --iuw-placeholder-destak-color: #417690; + --iuw-dropzone-background: rgba(255, 255, 255, 0.8); + --iuw-image-preview-border: #BFBFBF; + --iuw-image-preview-shadow: rgba(0, 0, 0, 0.3); + --iuw-add-image-background: #EFEFEF; + --iuw-add-image-color: #AAA; + + @media (prefers-color-scheme: dark) { + + } +} diff --git a/src/styles/_widget.scss b/src/styles/_widget.scss new file mode 100644 index 00000000..482b7ef1 --- /dev/null +++ b/src/styles/_widget.scss @@ -0,0 +1,41 @@ +.iuw-root, .iuw-root * { + box-sizing: border-box; +} + +.iuw-root { + /* base widget */ + @include make-root(); + + /* empty label */ + .iuw-empty { + @include make-empty(); + } + &:not(.non-empty) { + .iuw-empty { + height: 100%; + opacity: 1; + } + } + &.drop-zone { + .iuw-empty { + height: 0; + opacity: 0; + } + } + + /* drop label */ + .iuw-drop-label { + @include make-drop-label(); + } + &.drop-zone { + .iuw-drop-label { + height: 100%; + opacity: 1; + } + } + + /* image preview */ + .iuw-image-preview { + @include make-image-preview(); + } +} \ No newline at end of file