UNPKG

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
// @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); }); } });