comindware.core.ui
Version:
Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.
697 lines (599 loc) • 21.8 kB
JavaScript
// @flow
import PromiseService from '../../services/PromiseService';
import template from './templates/imageEditor.html';
import BaseCollectionEditorView from './base/BaseCollectionEditorView';
import formRepository from '../formRepository';
import LocalizationService from '../../services/LocalizationService';
import dropdown from 'dropdown';
import PanelView from './impl/datalist/views/PanelView';
import ImageBubbleItemView from './impl/document/views/ImageBubbleItemView';
import AttachmentsController from './impl/document/gallery/AttachmentsController';
import DocumentsCollection from './impl/document/collections/DocumentsCollection';
import WindowService from '../../services/WindowService';
import CropperEditorView from './CropperEditorView';
import MultiselectAddButtonView from './MultiselectAddButtonView';
const classes = {
dropZone: 'editor__drop-zone',
activeDropZone: 'editor__drop-zone_active',
collapsed: 'editor_collapsed',
listCollapsed: 'l-list_collapsed',
imageDisabled: 'image-item_disabled'
};
const displayFormats = {
SHOW_AS_CARDS: 'ShowAsCards',
SHOW_AS_PICTURE: 'ShowAsPicture'
};
const tempIdPrefix = 'temp.';
const serverTempIdPrefix = 'cmw.temp.';
const defaultOptions = options => ({
readonly: false,
allowDelete: true,
multiple: true,
fileFormat: '',
showRevision: true,
createImage: null,
editImage: null,
removeImage: null,
displayText: '',
isInline: false,
width: null,
height: null,
displayFormat: displayFormats.SHOW_AS_CARDS
});
const MAX_NUMBER_VISIBLE_DOCS_FOR_CARDS = 4;
const MAX_NUMBER_VISIBLE_DOCS_FOR_PICTURES = 1;
export default formRepository.editors.Image = BaseCollectionEditorView.extend({
initialize(options = {}) {
this.__applyOptions(options, defaultOptions);
this.displayFormat = this.options.displayFormat;
const cropHeight = this.options.height;
const cropWidth = this.options.width;
this.aspectRatio = cropWidth && cropHeight ? cropWidth / cropHeight : undefined;
this.collection = new DocumentsCollection(this.value);
this.listenTo(this.collection, 'attachments:remove', this.removeItem);
this.listenTo(this.collection, 'add remove reset', this.__onCollectionChange);
this.reqres = Backbone.Radio.channel(_.uniqueId('mSelect'));
this.reqres.reply('value:set', this.onValueAdd, this);
this.reqres.reply('value:navigate', this.onValueNavigate, this);
this.reqres.reply('search:more', this.onSearchMore, this);
this.reqres.reply('filter:text', this.onFilterText, this);
this.attachmentsController = new AttachmentsController();
if (this.canAdd) {
this.renderDropdown();
}
this._windowResize = _.throttle(this.update.bind(this), 100, true);
window.addEventListener('resize', this._windowResize);
this.createdUrls = [];
if (this.collection.length) {
this.__onCollapseClick();
}
this.$editorEl.attr('data-empty-text', LocalizationService.get('CORE.FORM.EDITORS.DOCUMENT.NODOCUMENT'));
},
canAdd: false,
className: 'editor editor_image',
template: Handlebars.compile(template),
childView: ImageBubbleItemView,
childViewContainer: '.js-collection-list',
childViewOptions() {
return {
attachmentsController: this.attachmentsController,
allowDelete: this.options.allowDelete,
showRevision: this.options.showRevision,
isInline: this.options.isInline,
readonly: this.readonly,
displayFormat: this.displayFormat,
editorHasHistory: false
};
},
collapsed: true,
isDropZoneCollapsed: false,
ui: {
collapseIcon: '.js-collapse-icon',
fileUploadButton: '.js-file-button',
fileUpload: '.js-file-input',
fileZone: '.js-file-zone',
showMoreBtn: '.js-more-btn',
invisibleCount: '.js-invisible-count',
listContainer: '.js-list',
showMoreText: '.js-more-btn-text',
showMoreContainer: '.js-show-more'
},
focusElement: '.js-file-button',
cropTemps: [],
templateContext() {
return Object.assign(this.options, {
displayText: LocalizationService.get('CORE.FORM.EDITORS.IMAGE.ADDIMAGE'),
placeHolderText: LocalizationService.get('CORE.FORM.EDITORS.IMAGE.DRAGFILE'),
multiple: this.options.multiple,
fileFormat: this.__adjustFileFormat(this.options.fileFormat),
showAsPicture: this.displayFormat === displayFormats.SHOW_AS_PICTURE
});
},
events() {
return {
'click @ui.collapseIcon': '__onCollapseClick',
'click @ui.showMoreBtn': 'toggleShowMore',
keydown: '__handleKeydown',
'change @ui.fileUpload': 'onSelectFiles',
'click @ui.fileUploadButton': '__onItemClick',
'dragenter @ui.fileZone': '__onDragenter',
'dragover @ui.fileZone': '__onDragover',
'dragleave @ui.fileZone': '__onDragleave',
'drop @ui.fileZone': '__onDrop'
};
},
setValue(value) {
this.__value(value);
this.update();
},
isTemp(objectId) {
return !objectId || objectId.startsWith(tempIdPrefix) || objectId.startsWith(serverTempIdPrefix);
},
getValue() {
this.syncValue();
return this.value;
},
isEmptyValue() {
return !this.collection.length;
},
onDestroy() {
window.removeEventListener('resize', this._windowResize, true);
this.createdUrls.forEach(url => window.URL.revokeObjectURL(url));
},
// override
checkChange() {},
__onDragenter(e) {
e.preventDefault();
e.stopPropagation();
this.ui.fileZone.addClass(classes.activeDropZone);
},
__onDragover(e) {
e.preventDefault();
e.stopPropagation();
const dataTransfer = e.originalEvent.dataTransfer;
if (!dataTransfer.items.length || !dataTransfer.types.includes('Files')) {
return;
}
if (this.readonly) {
dataTransfer.dropEffect = 'none';
} else {
dataTransfer.dropEffect = 'move';
}
},
__onDragleave(e) {
e.preventDefault();
e.stopPropagation();
if (e.target.classList.contains(classes.dropZone)) {
this.ui.fileZone.removeClass(classes.activeDropZone);
}
},
__onDrop(e) {
e.preventDefault();
e.stopPropagation();
const files = e.originalEvent.dataTransfer.files;
this.ui.fileZone.removeClass(classes.activeDropZone);
if (!files.length) {
return;
}
if (this.__validate(files) && !this.readonly) {
this._uploadFiles(files);
}
},
syncValue() {
this.value = this.collection
? this.collection.toJSON().map(m => {
const { file, isLoading, uniqueId, ...rest } = m;
return rest;
})
: [];
},
addItems(items) {
this.onValueAdd(items);
this.__updateEmpty();
},
removeItem(model) {
this.collection.remove(model);
this.options.removeImage?.(model.id);
this.__triggerChange();
},
async __value(value, triggerChange) {
if (this.value === value) {
return;
}
this.value = value;
if (value) {
this.files = [];
this.collection.forEach(model => {
model.set({ isLoading: false });
});
for (let i = 0; i < value.length; i++) {
if (this.isTemp(value[i].id) && !this.cropTemps.includes(value[i].id) && value[i].extension !== 'tiff') {
this.cropTemps.push(value[i].id);
await this.__getCropperPopup(value[i]);
}
}
if (this.files.length) {
this.__editElementsCollection(this.files);
this.files.forEach(model => {
model.uniqueId = this.options.editImage?.(model);
const savingModel = this.collection.findWhere({ id: model.id });
savingModel.set({ isLoading: true });
});
this.ui.fileZone.trigger('reset');
this.__triggerChange();
}
}
this.collection.reset(value || []);
if (triggerChange) {
this.__triggerChange();
}
},
__adjustFileFormat(fileFormat) {
if (!fileFormat) {
return '';
}
let result = '';
for (let i = 0; i < fileFormat.length; i += 1) {
result += `.${fileFormat[i].toLowerCase()}`;
if (i < fileFormat.length - 1) {
result += ',';
}
}
return result;
},
uploadUrl: '/api/UploadAttachment',
openFileUploadWindow() {
this.ui.fileUpload.click();
},
__onItemClick() {
this.ui.fileUpload.click();
},
onSelectFiles(e) {
if (this.internalChange) {
return;
}
const input = e.target;
const files = input.files;
if (this.__validate(files) && !this.readonly) {
this._uploadFiles(files);
}
},
_uploadFiles(fileList, items) {
this.trigger('beforeUpload');
this.trigger('set:loading', true);
const files = this.__tryImages(fileList);
if (items) {
Promise.resolve(this._readFileEntries(items)).then(fileEntrie => {
this._sendFilesToServer(fileEntrie);
});
} else if (files.length) {
this._sendFilesToServer(files);
}
},
__tryImages(fileList) {
return Array.prototype.filter.call(fileList, file => file.type.split('/')[0] === 'image');
},
// recursive loading of folders is currently supported only in chrome
// promise function
_readFileTree(itemEntry) {
return new Promise(resolve => {
if (itemEntry.isFile) {
itemEntry.file(file => {
resolve(file);
});
} else if (itemEntry.isDirectory) {
const dirReader = itemEntry.createReader();
dirReader.readEntries(entries => {
Promise.map(entries, entry => this._readFileTree(entry)).then(() => {
resolve(_.flatten(arguments));
});
});
}
});
},
//typeof filesEntries is array of DataTransfer
_readFileEntries(fileEntries) {
const deferrArray = [];
for (let i = 0; i < fileEntries.length; i++) {
let entry;
if (fileEntries[i].getAsEntry) {
//Standard HTML5 API
entry = fileEntries[i].getAsEntry();
} else if (fileEntries[i].webkitGetAsEntry) {
//WebKit implementation of HTML5 API.
entry = fileEntries[i].webkitGetAsEntry();
}
if (entry) {
deferrArray.push(this.readFileTree(entry));
}
}
return Promise.all(deferrArray).then(() => _.flatten(arguments));
},
_sendFilesToServer(files) {
const form = new FormData();
const length = this.options.multiple === false ? 1 : files.length;
const resultObjects = [];
for (let i = 0; i < length; i++) {
form.append(`file${i + 1}`, files[i]);
const currFileName = files[i].name;
const tempUrl = window.URL.createObjectURL(files[i]);
this.createdUrls.push(tempUrl);
const obj = {
name: currFileName,
extension: currFileName ? currFileName.replace(/.*\./g, '') : '',
url: tempUrl,
file: files[i],
isLoading: true,
uniqueId: _.uniqueId('image-')
};
resultObjects.push(obj);
}
this.trigger('uploaded', resultObjects);
if (this.options.multiple === false) {
this.collection.reset();
}
this.addItems(resultObjects);
const config = {
url: this.uploadUrl,
data: form,
processData: false,
type: 'POST',
contentType: false,
encoding: 'multipart/form-data',
enctype: 'multipart/form-data',
mimeType: 'multipart/form-data',
success: data => {
if (this.isDestroyed()) {
return;
}
const tempResult = JSON.parse(data);
for (let i = 0; i < tempResult.fileIds.length; i++) {
const model = this.collection.findWhere({ uniqueId: resultObjects[i]?.uniqueId });
if (model) {
const streamId = tempResult.fileIds[i];
model.set({
streamId,
isLoading: false
});
const id = this.options.createImage?.(model.toJSON());
if (id) {
model.set({ id });
}
model.set({ uniqueId: id || streamId });
}
}
this.internalChange = true;
this.ui.fileUpload[0].value = null;
this.internalChange = false;
this.ui.fileZone.trigger('reset');
this.__triggerChange();
this.trigger('set:loading', false);
},
error: () => {
this.trigger('set:loading', false);
this.trigger('failed');
}
};
// eslint-disable-next-line no-undef
PromiseService.registerPromise($.ajax(config), true);
},
__editElementsCollection(resultObjects) {
resultObjects.forEach(changeModel => {
this.collection.remove(this.collection.find(model => model.id === changeModel.id));
this.collection.add(changeModel);
});
},
__validate(files) {
let ext = '';
let fileFormat = this.options.fileFormat.toLowerCase();
let incorrectFileNames = '';
if (!files) {
return false;
}
if (!fileFormat) {
return true;
}
fileFormat = fileFormat.toLowerCase();
for (let i = 0; i < files.length; i += 1) {
ext = files[i].name
.split('.')
.pop()
.toLowerCase();
if (fileFormat.indexOf(ext) === -1) {
incorrectFileNames += `${files[i].name}, `;
}
}
// TODO: show validation error
this.trigger('validation:error', incorrectFileNames);
return !incorrectFileNames;
},
renderDropdown() {
this.dropdownModel = {
button: {
text: LocalizationService.get('CORE.FORM.EDITORS.DOCUMENT.ADD')
},
panel: {
collection: this.controller.collection,
totalCount: this.controller.totalCount || 0
}
};
this.dropdownView = dropdown.factory.createDropdown({
buttonView: MultiselectAddButtonView,
buttonViewOptions: {
model: this.dropdownModel.button,
reqres: this.reqres
},
panelView: PanelView,
panelViewOptions: {
model: this.dropdownModel.panel,
reqres: this.reqres
}
});
this.$selectButtonEl.html(this.dropdownView.render().$el);
},
showDropDown() {
this.dropdownView.open();
},
onAttach() {
this.renderShowMore();
},
onValueAdd(model) {
this.collection.add(model);
this.trigger('valueAdded', model);
this.dropdownView && this.dropdownView.close();
this.renderShowMore();
},
onValueNavigate() {
//todo
this.controller.navigate(this.getValue());
},
onSearchMore() {
// TODO: Not implemented in Release 1
this.dropdownView.close();
},
onFilterText(options) {
return this.controller.fetch(options).then(() => this.dropdownModel.get('panel').set('totalCount', this.controller.totalCount));
},
toggleShowMore() {
this.collapsed = !this.collapsed;
this.renderShowMore();
},
renderShowMore() {
if (this.collapsed) {
this.collapseShowMore();
} else {
this.expandShowMore();
}
this.ui.listContainer.toggleClass(classes.listCollapsed, this.collapsed);
},
update() {
if (this.collapsed && this.isRendered()) {
this.$container.children().show();
this.collapseShowMore();
}
},
collapseShowMore() {
if (this.isDestroyed()) {
return;
}
const documentElements = this.$container.children();
if (documentElements.length === 0) {
this.ui.showMoreContainer.hide();
return;
}
const childViews = documentElements;
const length = this.collection.length;
// invisible children
const isShowAsCards = this.displayFormat === displayFormats.SHOW_AS_CARDS;
const maxNumbers = isShowAsCards ? MAX_NUMBER_VISIBLE_DOCS_FOR_CARDS : MAX_NUMBER_VISIBLE_DOCS_FOR_PICTURES;
for (let i = maxNumbers; i < length; i++) {
const child = childViews[i];
if (isShowAsCards && i < maxNumbers + 2) {
child.classList.add(classes.imageDisabled);
continue;
}
child.style.display = 'none';
}
const countInvisibleElements = length - maxNumbers;
if (countInvisibleElements > 0) {
this.ui.showMoreContainer.show();
this.ui.invisibleCount.html(countInvisibleElements);
this.ui.showMoreText.html(`${LocalizationService.get('CORE.FORM.EDITORS.DOCUMENT.SHOWMORE')} `);
} else {
this.ui.showMoreContainer.hide();
}
},
expandShowMore() {
this.$container
.children()
.removeClass(classes.imageDisabled)
.show();
this.ui.showMoreText.html(LocalizationService.get('CORE.FORM.EDITORS.DOCUMENT.HIDE'));
this.ui.invisibleCount.html('');
},
__handleKeydown(e) {
switch (e.keyCode) {
case 13:
case 40:
this.dropdownView.open();
break;
default:
break;
}
},
__onCollectionChange() {
this.isDropZoneCollapsed = Boolean(this.collection?.length);
this.__updateCollapseButton();
},
__onCollapseClick() {
const collapsed = this.isDropZoneCollapsed;
this.isDropZoneCollapsed = !collapsed;
this.__updateCollapseButton();
},
__updateCollapseButton() {
this.$el.toggleClass(classes.collapsed, this.isDropZoneCollapsed);
},
__getCropperPopup(file) {
this.trigger('set:loading', true);
return new Promise(resolve => {
const cropperView = new CropperEditorView({
file,
cropOptions: {
aspectRatio: this.aspectRatio
}
});
const cropPopup = new Core.layout.Popup({
size: {
width: 'auto',
height: 'auto'
},
header: LocalizationService.get('CORE.FORM.EDITORS.IMAGE.CROPPER'),
onClose() {
resolve(true);
},
buttons: [
{
id: false,
text: LocalizationService.get('CORE.FORM.EDITORS.IMAGE.CANCELCROP'),
isCancel: true,
handler() {
resolve(true);
}
},
{
id: true,
text: LocalizationService.get('CORE.FORM.EDITORS.IMAGE.CUT'),
handler() {
cropPopup.trigger('save:upload', cropperView.getCrop());
resolve(true);
}
}
],
content: new Core.layout.Form({
model: new Backbone.Model(),
schema: [
{
type: 'v-container',
items: [
{
id: 'expression',
name: 'Computed Expression',
view: cropperView
}
]
}
]
})
});
this.listenTo(cropPopup, 'save:upload', changedFile => {
if (changedFile.imageTransformations.length) {
this.files.push(changedFile);
}
});
WindowService.showPopup(cropPopup);
}).then(() => {
WindowService.closePopup();
this.trigger('set:loading', false);
});
}
});