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 %}
+
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