import * as React from "react"; import * as Constants from "~/common/constants"; import * as SVG from "~/common/svg"; import * as Strings from "~/common/strings"; import * as Utilities from "~/common/utilities"; import * as Window from "~/common/window"; import * as Validations from "~/common/validations"; import * as UserBehaviors from "~/common/user-behaviors"; import * as Events from "~/common/custom-events"; import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview"; import CTATransition from "~/components/core/CTATransition"; import { Link } from "~/components/core/Link"; import { GlobalCarousel } from "~/components/system/components/GlobalCarousel"; import { CheckBox } from "~/components/system/components/CheckBox"; import { css } from "@emotion/react"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { Toggle } from "~/components/system/components/Toggle"; import { Tooltip } from "~/components/core/Tooltip"; import { ButtonPrimary, ButtonSecondary, ButtonDisabled, ButtonWarning, } from "~/components/system/components/Buttons"; import { GroupSelectable, Selectable } from "~/components/core/Selectable/"; import { ConfirmationModal } from "~/components/core/ConfirmationModal"; //NOTE(martina): sets 200px as the standard width for a 1080px wide layout with 20px margin btwn images. //If the container is larger or smaller, it scales accordingly by that factor const MIN_SIZE = 10; const SIZE = 200; const MARGIN = 20; const CONTAINER_SIZE = 5 * SIZE + 4 * MARGIN; const TAG_HEIGHT = 20; const generateLayout = (items) => { if (!items) { return []; } if (!items.length) { return []; } return ( items.map((item, i) => { return { x: (i % 5) * (SIZE + MARGIN), y: 0, w: SIZE, h: 0, z: 0, id: item.id.replace("data-", ""), }; }) || [] ); }; const preload = (item) => new Promise((resolve, reject) => { const url = Utilities.getImageUrlIfExists(item); if (!url) { resolve(200); } const img = new Image(); img.onload = () => { resolve((200 * img.height) / img.width); }; img.onerror = reject; img.src = url; }); const STYLES_MOBILE_HIDDEN = css` @media (max-width: ${Constants.sizes.mobile}px) { display: none; } `; const STYLES_COPY_INPUT = css` pointer-events: none; position: absolute; opacity: 0; `; const STYLES_LOADER = css` display: flex; align-items: center; justify-content: center; height: calc(100vh - 400px); width: 100%; `; const STYLES_EDIT_CONTAINER = css` border: 1px solid ${Constants.system.gray}; padding: 24px; overflow: hidden; `; const STYLES_CONTAINER = css` width: 100%; position: relative; height: 100vh; z-index: ${Constants.zindex.body}; overflow: hidden; `; const STYLES_CONTAINER_EDITING = css` ${STYLES_CONTAINER} background-image: radial-gradient( ${Constants.system.grayLight2} 10%, transparent 0 ); background-size: 30px 30px; background-position: -50% -50%; `; const STYLES_BUTTONS_ROW = css` display: flex; flex-direction: row; align-items: center; `; const STYLES_TOOLTIP_ANCHOR = css` border: 1px solid #f2f2f2; background-color: ${Constants.system.white}; border-radius: 4px; display: flex; flex-direction: column; gap: 12px; align-items: flex-start; justify-content: space-around; box-shadow: 0px 8px 24px rgba(178, 178, 178, 0.2); width: 275px; height: auto; position: absolute; top: 0px; left: 50px; z-index: ${Constants.zindex.tooltip}; `; const STYLES_TOOLTIP_TEXT = css` padding: 0 12px 0 12px; font-family: ${Constants.font.text}; font-size: 12px; `; const STYLES_TOGGLE_BOX = css` ${STYLES_BUTTONS_ROW} border: 1px solid ${Constants.system.gray}; border-radius: 4px; height: 40px; padding: 0 16px; `; const STYLES_ITEM = css` position: absolute; transform-origin: top left; cursor: pointer; -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Safari */ -khtml-user-select: none; /* Konqueror HTML */ -moz-user-select: none; /* Old versions of Firefox */ -ms-user-select: none; /* Internet Explorer/Edge */ user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ `; const STYLES_ITEM_EDITING = css` ${STYLES_ITEM} cursor: grab; :active { cursor: grabbing; } `; const STYLES_FILE_TAG = css` font-family: ${Constants.font.text}; color: ${Constants.system.grayDark2}; display: flex; align-items: center; width: 100%; padding: 0 4px; background: ${Constants.system.white}; `; const STYLES_FILE_NAME = css` width: 100%; min-width: 10%; overflow: hidden; text-wrap: nowrap; white-space: nowrap; text-overflow: ellipsis; text-align: left; `; const STYLES_FILE_TYPE = css` color: ${Constants.system.grayLight2}; text-transform: uppercase; flex-shrink: 0; margin-left: 16px; text-align: right; `; const STYLES_HANDLE_BOX = css` cursor: nwse-resize; position: absolute; bottom: 0px; right: 0px; height: 24px; width: 24px; background-color: rgba(248, 248, 248, 0.6); color: #4b4a4d; border-radius: 24px 0px 0px 0px; `; const STYLES_ACTION_BAR = css` display: flex; align-items: center; justify-content: space-between; border-radius: 4px; padding: 0px 32px; box-sizing: border-box; background-color: ${Constants.semantic.textGrayDark}; width: 90vw; max-width: 878px; height: 48px; @media (max-width: ${Constants.sizes.mobile}px) { display: none; } `; const STYLES_ACTION_BAR_CONTAINER = css` position: fixed; bottom: 12px; left: 0px; width: 100vw; display: flex; justify-content: center; z-index: ${Constants.zindex.header}; left: 10vw; width: 80vw; @media (max-width: ${Constants.sizes.mobile}px) { display: none; } `; const STYLES_RIGHT = css` flex-shrink: 0; display: flex; align-items: center; `; const STYLES_LEFT = css` width: 100%; min-width: 10%; display: flex; align-items: center; `; const STYLES_FILES_SELECTED = css` font-family: ${Constants.font.semiBold}; color: ${Constants.system.white}; @media (max-width: ${Constants.sizes.mobile}px) { display: none; } `; const STYLES_ICON_BOX = css` height: 32px; width: 32px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; margin-left: 16px; `; const STYLES_ICON_CIRCLE = css` height: 24px; width: 24px; border-radius: 50%; background-color: rgba(248, 248, 248, 0.6); color: #4b4a4d; display: flex; align-items: center; justify-content: center; cursor: pointer; margin: 0 8px; -webkit-backdrop-filter: blur(25px); backdrop-filter: blur(25px); `; const STYLES_ICON_ROW = css` display: flex; flex-direction: row; position: absolute; left: calc(50% - 40px); `; export class SlateLayout extends React.Component { _ref; _input; keysPressed = {}; state = { unit: 10, items: this.props.items, layout: this.props.layout || generateLayout(this.props.items), hover: null, containerHeight: 1000, prevLayouts: [], zIndexMax: this.props.layout && this.props.layout.length ? Math.max(...this.props.layout.map((pos) => pos.z)) + 1 : 1, fileNames: this.props.fileNames, defaultLayout: this.props.layout ? this.props.defaultLayout : true, editing: false, show: false, checked: {}, copyValue: "", tooltip: null, keyboardTooltip: false, modalShowDeleteFiles: false, }; componentDidMount = async () => { if (!this.state.editing) window.addEventListener("keydown", this._handleUncheckAll); this.debounceInstance = Window.debounce(this._recalculate, 250); window.addEventListener("resize", this.debounceInstance); await this.calculateUnit(); if (this.props.layout) { let layout = await this.repairLayout(this.state.items); if (layout) { this.setState({ show: true, layout }); this.props.onSaveLayout( { ver: "2.0", fileNames: this.state.fileNames, defaultLayout: this.state.defaultLayout, layout, }, true ); } else { this.setState({ show: true }); } } else { let layout = await this.calculateLayout(); this.props.onSaveLayout( { ver: "2.0", fileNames: this.state.fileNames, defaultLayout: this.state.defaultLayout, layout, }, true ); this.setState({ show: true, layout }); } this.calculateContainer(); }; componentWillUnmount = () => { window.removeEventListener("resize", this.debounceInstance); if (this.state.editing) { window.removeEventListener("keydown", this._handleKeyDown); window.removeEventListener("keyup", this._handleKeyUp); } else { window.removeEventListener("keydown", this._handleUncheckAll); } }; componentDidUpdate = async (prevProps) => { if (prevProps.items.length !== this.props.items.length) { //NOTE(martina): to handle when items are added / deleted from the slate, and recalculate the layout //NOTE(martina): if there is a case that allows simultaneous add / delete (aka modify but same length), this will not work. //would need to replace it with event listener + custom events let layout = await this.repairLayout(this.props.items); if (layout) { await this.setState({ layout, items: this.props.items }); this.calculateContainer(); if (!this.state.editing) { this.props.onSaveLayout( { ver: "2.0", fileNames: this.state.fileNames, defaultLayout: this.state.defaultLayout, layout, }, true ); } } } }; _recalculate = async () => { let prevUnit = this.state.unit; await this.calculateUnit(); this.setState({ containerHeight: this.state.containerHeight * (this.state.unit / prevUnit) }); }; calculateUnit = () => { let ref = this._ref; if (!ref) { return; } let unit = ref.clientWidth / CONTAINER_SIZE; if (unit === 0) { return; } this.setState({ unit }); }; calculateContainer = () => { let highestPoints = this.state.layout.map((pos) => { return pos.y + pos.h; }); let containerHeight = Math.max(...highestPoints) * this.state.unit; this.setState({ containerHeight }); }; repairLayout = async (items, layouts) => { let defaultLayout = layouts ? layouts.defaultLayout : this.state.defaultLayout; let fileNames = layouts ? layouts.fileNames : this.state.fileNames; let layout = layouts ? this.cloneLayout(layouts.layout) : this.cloneLayout(this.state.layout); let layoutIds = layout.map((pos) => pos.id.replace("data-", "")); let repairNeeded = false; if (items.length !== layout.length) { repairNeeded = true; } if (!repairNeeded && defaultLayout) { for (let i = 5; i < layout.length; i++) { if (!layout[i].y) { repairNeeded = true; } } } if (!repairNeeded && items.length === layout.length) { let itemIds = items.map((item) => item.id.replace("data-", "")); for (let i = 0; i < itemIds.length; i++) { if (itemIds[i] !== layoutIds[i]) { repairNeeded = true; break; } } if (!repairNeeded) { return; } } let newLayout = new Array(items.length); for (let i = 0; i < items.length; i++) { let layoutIndex = layoutIds.indexOf(items[i].id.replace("data-", "")); if (layoutIndex === -1) { continue; } else if (defaultLayout && layoutIndex >= 5 && !layout[layoutIndex].y) { //NOTE(martina): to catch ones that were not preloaded correctly before and patch them continue; } else { newLayout[i] = layout[layoutIndex]; } } let added = []; for (let i = 0; i < newLayout.length; i++) { if (!newLayout[i]) { added.push(items[i]); } } let results = await Promise.allSettled(added.map((item) => preload(item))); let heights = results.map((result) => { if (result.status === "fulfilled") { return result.value; } else { return 200; } }); let yMax; if (!defaultLayout) { let highestPoints = layout.map((pos) => { return pos.y + pos.h; }); yMax = Math.max(...highestPoints) + MARGIN; if (fileNames) { yMax += TAG_HEIGHT; } yMax = yMax || 0; } let h = 0; for (let i = 0; i < newLayout.length; i++) { if (!newLayout[i]) { let itemAbove = h - 5 < 0 ? null : newLayout[i - 5]; let height = heights[h]; newLayout[i] = { x: defaultLayout ? (i % 5) * (SIZE + MARGIN) : (h % 5) * (SIZE + MARGIN), y: defaultLayout ? 0 : itemAbove ? fileNames ? itemAbove.y + itemAbove.h + MARGIN + TAG_HEIGHT : itemAbove.y + itemAbove.h + MARGIN : yMax, h: height, w: SIZE, z: 0, id: items[i].id.replace("data-", ""), }; h += 1; } } if (defaultLayout) { for (let i = 0; i < newLayout.length; i++) { let itemAbove = i - 5 < 0 ? null : newLayout[i - 5]; newLayout[i].x = (i % 5) * (SIZE + MARGIN); newLayout[i].y = itemAbove ? fileNames ? itemAbove.y + itemAbove.h + MARGIN + TAG_HEIGHT : itemAbove.y + itemAbove.h + MARGIN : 0; } } return newLayout; }; calculateLayout = async (oldLayout) => { let heights = await this.calculateHeights(); let layout = oldLayout ? oldLayout : this.state.layout; for (let i = 0; i < this.state.items.length; i++) { let height = heights[i]; let itemAbove = i - 5 < 0 ? null : layout[i - 5]; layout[i] = { x: (i % 5) * (SIZE + MARGIN), y: itemAbove ? this.state.fileNames ? itemAbove.y + itemAbove.h + MARGIN + TAG_HEIGHT : itemAbove.y + itemAbove.h + MARGIN : 0, w: SIZE, h: oldLayout && oldLayout.length > i ? oldLayout[i].h || height : height, z: 0, id: this.state.items[i].id.replace("data-", ""), }; } return layout; }; calculateHeights = async () => { let results = await Promise.allSettled( this.state.items.map((item, i) => preload(item.coverImage ? item.coverImage : item, i)) ); let heights = results.map((result) => { if (result.status === "fulfilled") { return result.value || 200; } else { return 200; } }); return heights; }; cloneLayout = (layout) => { let copy = []; for (let pos of layout) { copy.push({ ...pos }); } return copy; }; _toggleEditing = async (e, discardChanges) => { if (this.state.editing) { window.removeEventListener("keydown", this._handleKeyDown); window.removeEventListener("keyup", this._handleKeyUp); if (discardChanges) { let layout = await this.repairLayout(this.state.items, this.state.savedProperties); let { fileNames, defaultLayout } = this.state.savedProperties; if ( layout || fileNames !== this.state.fileNames || defaultLayout !== this.state.defaultLayout ) { this.props.onSaveLayout( { ver: "2.0", fileNames, defaultLayout, layout, }, true ); } await this.setState({ editing: false, fileNames, defaultLayout, layout: layout ? layout : this.state.savedProperties.layout, prevLayouts: [], }); this.calculateContainer(); } else { await this.setState({ editing: false, prevLayouts: [] }); } } else { window.addEventListener("keydown", this._handleKeyDown); window.addEventListener("keyup", this._handleKeyUp); await this.setState({ editing: true, savedProperties: { defaultLayout: this.state.defaultLayout, fileNames: this.state.fileNames, layout: this.cloneLayout(this.state.layout), }, }); } this.calculateUnit(); }; _toggleFileNames = (e) => { if (!this.state.defaultLayout) { this.setState({ fileNames: !this.state.fileNames, prevLayouts: [ ...this.state.prevLayouts, { defaultLayout: this.state.defaultLayout, fileNames: this.state.fileNames, layout: this.cloneLayout(this.state.layout), }, ], }); } else { let layout = this.cloneLayout(this.state.layout); for (let i = 5; i < this.state.items.length; i++) { let itemAbove = layout[i - 5]; if (this.state.fileNames) { layout[i].y = itemAbove.y + itemAbove.h + MARGIN; } else { layout[i].y = itemAbove.y + itemAbove.h + MARGIN + TAG_HEIGHT; } } this.setState({ layout, fileNames: !this.state.fileNames, prevLayouts: [ ...this.state.prevLayouts, { defaultLayout: this.state.defaultLayout, fileNames: this.state.fileNames, layout: this.cloneLayout(this.state.layout), }, ], }); } }; _handleUncheckAll = (e) => { let numChecked = Object.keys(this.state.checked).length; if (!this.state.editing && e.keyCode === 27 && numChecked) this.setState({ checked: {} }); }; _handleKeyDown = (e) => { let prevValue = this.keysPressed[e.key]; this.keysPressed[e.key] = true; if ( (this.keysPressed["Control"] || this.keysPressed["Meta"]) && this.keysPressed["z"] && prevValue !== this.keysPressed[e.key] ) { e.preventDefault(); e.stopPropagation(); this._handleUndo(); } else if ( (this.keysPressed["Control"] || this.keysPressed["Meta"]) && this.keysPressed["s"] && prevValue !== this.keysPressed[e.key] ) { e.preventDefault(); e.stopPropagation(); this._handleSaveLayout(); } else if ( (this.keysPressed["Control"] || this.keysPressed["Meta"]) && prevValue !== this.keysPressed[e.key] ) { e.preventDefault(); e.stopPropagation(); } }; _handleKeyUp = (e) => { this.keysPressed[e.key] = false; this.keysPressed = {}; }; _handleUndo = () => { if (this.state.prevLayouts.length) { let prevLayouts = this.state.prevLayouts; let layouts = prevLayouts.pop(); this.setState({ ...layouts, prevLayouts }); } }; _addSelectedItemsOnDrag = (e) => { let selectedItems = {}; for (const i of e) { selectedItems[i] = true; } this.setState({ checked: { ...this.state.checked, ...selectedItems } }); }; _removeSelectedItemsOnDrag = (e) => { const selectedItems = { ...this.state.checked }; for (const i in selectedItems) { selectedItems[i] = selectedItems[i] && !e.includes(+i); if (!selectedItems[i]) delete selectedItems[i]; } this.setState({ checked: selectedItems, ...selectedItems }); }; _handleDragAndSelect = (e, { isAltDown }) => { if (isAltDown) { this._removeSelectedItemsOnDrag(e); return; } this._addSelectedItemsOnDrag(e); }; _handleMouseDown = (e, i) => { e.stopPropagation(); e.preventDefault(); let layout = this.cloneLayout(this.state.layout); layout[i].z = this.state.zIndexMax; this.setState({ xStart: e.clientX, yStart: e.clientY, dragIndex: i, origLayout: this.cloneLayout(layout), layout, zIndexMax: this.state.zIndexMax + 1, prevLayouts: [ ...this.state.prevLayouts, { defaultLayout: this.state.defaultLayout, fileNames: this.state.fileNames, layout: this.cloneLayout(this.state.layout), }, ], }); window.addEventListener("mousemove", this._handleDrag); window.addEventListener("mouseup", this._handleMouseUp); }; _handleDrag = (e) => { e.preventDefault(); e.stopPropagation(); let layout = this.cloneLayout(this.state.origLayout); let pos = layout[this.state.dragIndex]; let dX = (e.clientX - this.state.xStart) / this.state.unit; let dY = (e.clientY - this.state.yStart) / this.state.unit; if (e.shiftKey) { if (Math.abs(dY) > Math.abs(dX)) { pos.y += dY; } else { pos.x += dX; } } else { pos.x += dX; pos.y += dY; } if (pos.x >= CONTAINER_SIZE || pos.x + pos.w <= 0 || pos.y + pos.h <= 0) { return; } this.setState({ layout }); }; _handleDownloadFiles = async () => { const selectedFiles = this.props.items.filter((_, i) => this.state.checked[i]); UserBehaviors.compressAndDownloadFiles({ files: selectedFiles, resourceURI: this.props.resources.download, }); this.setState({ checked: {} }); }; _handleMouseUp = (e) => { window.removeEventListener("mousemove", this._handleDrag); window.removeEventListener("mouseup", this._handleMouseUp); let layout = this.state.layout; let pos = layout[this.state.dragIndex]; if (!e.ctrlKey && !e.metaKey) { pos.x = Math.round(pos.x / 10) * 10; pos.y = Math.round(pos.y / 10) * 10; } let state = { dragIndex: null, layout }; if (this.state.defaultLayout) { if (e.clientX !== this.state.xStart || e.clientX !== this.state.yStart) { state.defaultLayout = false; } } if ((pos.y + pos.h) * this.state.unit > this.state.containerHeight) { state.containerHeight = (pos.y + pos.h) * this.state.unit; } this.setState(state); }; _handleMouseDownResize = (e, i) => { e.stopPropagation(); e.preventDefault(); let layout = this.cloneLayout(this.state.layout); layout[i].z = this.state.zIndexMax; this.setState({ xStart: e.clientX, yStart: e.clientY, dragIndex: i, origLayout: this.cloneLayout(layout), layout, zIndexMax: this.state.zIndexMax + 1, prevLayouts: [ ...this.state.prevLayouts, { defaultLayout: this.state.defaultLayout, fileNames: this.state.fileNames, layout: this.cloneLayout(this.state.layout), }, ], ratio: [layout[i].w, layout[i].h], freeRatio: !Validations.isPreviewableImage(this.state.items[i].type), }); window.addEventListener("mousemove", this._handleDragResize); window.addEventListener("mouseup", this._handleMouseUpResize); }; _handleDragResize = (e) => { let state = {}; e.preventDefault(); e.stopPropagation(); let layout = this.cloneLayout(this.state.origLayout); let pos = layout[this.state.dragIndex]; let dX = (e.clientX - this.state.xStart) / this.state.unit; let dY; if (this.state.freeRatio && !e.shiftKey) { dY = (e.clientY - this.state.yStart) / this.state.unit; } else { dY = (dX * this.state.ratio[1]) / this.state.ratio[0]; } pos.w += dX; pos.h += dY; if ( pos.w < MIN_SIZE || pos.h < MIN_SIZE || pos.w > CONTAINER_SIZE || pos.x >= CONTAINER_SIZE || pos.x + pos.w <= 0 || pos.y + pos.h <= 0 ) { return; } this.setState({ layout, ...state }); }; _handleMouseUpResize = (e) => { window.removeEventListener("mousemove", this._handleDragResize); window.removeEventListener("mouseup", this._handleMouseUpResize); let layout = this.state.layout; let pos = layout[this.state.dragIndex]; if (!e.ctrlKey && !e.metaKey) { pos.w = Math.round(pos.w / 10) * 10; if (this.state.freeRatio) { pos.h = Math.round(pos.h / 10) * 10; } else { pos.h = (pos.w * this.state.ratio[1]) / this.state.ratio[0]; } } let state = { dragIndex: null, layout, ratio: null }; if (this.state.defaultLayout) { if (e.clientX !== this.state.xStart || e.clientX !== this.state.yStart) { state.defaultLayout = false; } } if ((pos.y + pos.h) * this.state.unit > this.state.containerHeight) { state.containerHeight = (pos.y + pos.h) * this.state.unit; } this.setState(state); }; _handleResetLayout = async (res) => { if (!res) { this.setState({ modalShowResetLayout: false }); return; } let prevLayout = this.cloneLayout(this.state.layout); let layout = await this.calculateLayout(); this.setState({ defaultLayout: true, prevLayouts: [ ...this.state.prevLayouts, { defaultLayout: this.state.defaultLayout, fileNames: this.state.fileNames, layout: prevLayout, }, ], layout, zIndexMax: 1, modalShowResetLayout: false, }); }; _handleSaveLayout = async () => { //NOTE(martina): collapses the z-indexes back down to 0 through n-1 (so they don't continuously get higher) let zIndexes = this.state.layout.map((pos) => pos.z); zIndexes = [...new Set(zIndexes)]; zIndexes.sort(function (a, b) { return a - b; }); let layout = this.cloneLayout(this.state.layout); for (let pos of layout) { pos.z = zIndexes.indexOf(pos.z); } await this.props.onSaveLayout({ ver: "2.0", fileNames: this.state.fileNames, defaultLayout: this.state.defaultLayout, layout: layout, }); await this.setState({ layout }); this._toggleEditing(); }; _handleCheckBox = (e) => { let checked = this.state.checked; if (e.target.value === false) { delete checked[e.target.name]; this.setState({ checked }); return; } this.setState({ checked: { ...this.state.checked, [e.target.name]: true }, }); }; _handleCopy = (e, value) => { e.stopPropagation(); e.preventDefault(); this.setState({ copyValue: value }, () => { this._input.select(); document.execCommand("copy"); }); }; _handleSetPreview = (e, i) => { e.stopPropagation(); e.preventDefault(); let url = Strings.getURLfromCID(this.state.items[i].cid); if (this.props.preview === url) return; this.props.onSavePreview(url); }; _handleDownload = async (e, i) => { e.stopPropagation(); e.preventDefault(); if (!this.props.viewer) { Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); return; } if (i !== undefined) { const file = this.state.items[i]; const response = await UserBehaviors.download(file); Events.hasError(response); } }; _handleSaveCopy = async (e, i) => { if (!this.props.viewer) { Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); return; } e.stopPropagation(); e.preventDefault(); let items = []; if (i !== undefined) { items = [this.state.items[i]]; } else { this.setState({ checked: {} }); for (let i of Object.keys(this.state.checked)) { items.push(this.state.items[i]); } } UserBehaviors.saveCopy({ files: items }); }; _handleAddToSlate = (e, i) => { if (!this.props.viewer) { Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); return; } e.stopPropagation(); e.preventDefault(); let items = []; if (i !== undefined) { items = [this.state.items[i]]; } else if (Object.keys(this.state.checked).length) { for (let index of Object.keys(this.state.checked)) { items.push(this.state.items[index]); } } this.setState({ checked: {} }); this.props.onAction({ type: "SIDEBAR", value: "SIDEBAR_ADD_FILE_TO_SLATE", data: { files: items, fromSlate: true }, }); }; _handleRemoveFromSlate = (e, i) => { e.stopPropagation(); e.preventDefault(); let ids = []; if (i !== undefined) { ids = [this.state.items[i].id.replace("data-", "")]; } else { for (let index of Object.keys(this.state.checked)) { ids.push(this.state.items[index].id.replace("data-", "")); } this.setState({ checked: {} }); } let slates = this.props.viewer.slates; let slateId = this.props.data.id; for (let slate of slates) { if (slate.id === slateId) { slate.objects = slate.objects.filter((obj) => !ids.includes(obj.id.replace("data-", ""))); this.props.onAction({ type: "UPDATE_VIEWER", viewer: { slates } }); break; } } UserBehaviors.removeFromSlate({ slate: this.props.data, ids }); }; _stopPropagation = (e) => e.stopPropagation(); _disableDragAndDropUploadEvent = () => { document.addEventListener("dragenter", this._stopPropagation); document.addEventListener("drop", this._stopPropagation); }; _enableDragAndDropUploadEvent = () => { document.removeEventListener("dragenter", this._stopPropagation); document.removeEventListener("drop", this._stopPropagation); }; _handleDragToDesktop = (e, object) => { const url = Strings.getURLfromCID(object.cid); const title = object.filename || object.data.name; const type = object.data.type; e.dataTransfer.setData("DownloadURL", `${type}:${title}:${url}`); }; _handleDeleteModal = () => { this.setState({ modalShowDeleteFiles: true }); }; _handleDeleteFiles = async (res, i) => { if (!res) { this.setState({ modalShowDeleteFiles: false }); return; } let ids = []; if (i !== undefined) { ids = [this.state.items[i].id.replace("data-", "")]; } else { for (let index of Object.keys(this.state.checked)) { ids.push(this.state.items[index].id.replace("data-", "")); } } let slates = this.props.viewer.slates; let slateId = this.props.data.id; for (let slate of slates) { if (slate.id === slateId) { slate.objects = slate.objects.filter( (obj) => !ids.includes(obj.id.replace("data-", "")) && !cids.includes(obj.cid) ); this.props.onAction({ type: "UPDATE_VIEWER", viewer: { slates } }); break; } } await UserBehaviors.deleteFiles(ids); this.setState({ checked: {}, modalShowDeleteFiles: false }); }; _stopProp = (e) => { e.stopPropagation(); e.preventDefault(); }; _handleLoginModal = (e) => { e.preventDefault(); e.stopPropagation(); Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); }; render() { let numChecked = Object.keys(this.state.checked).length; let unit = this.state.unit; return (
{this.props.isOwner ? ( this.state.editing ? (
Display titles
{this.state.defaultLayout ? ( Reset layout ) : ( { this.setState({ modalShowResetLayout: true }); }} style={{ marginRight: 16 }} > Reset layout )} {this.state.modalShowResetLayout && ( )} {this.state.prevLayouts.length ? ( Undo ) : ( Undo )}
this.setState({ keyboardTooltip: false })} > this.setState({ keyboardTooltip: true })} /> {this.state.keyboardTooltip ? (

Keyboard shortcuts

shift + drag

keep x value or y value while moving file

shift + resize

keep aspect ratio while resizing

ctrl + drag

move without snapping to the dot grid

ctrl + resize

resize without snapping to the dot grid

) : null}
this._toggleEditing(e, true)} style={{ cursor: "pointer", marginLeft: 16 }} > Cancel Save
) : (
Edit layout
) ) : null}
{ this._ref = c; }} > {this.state.show ? ( this.state.layout.map((pos, i) => ( { this._disableDragAndDropUploadEvent(); this._handleDragToDesktop(e, this.state.items[i]); }} onDragEnd={this._enableDragAndDropUploadEvent} selectableKey={i} onMouseEnter={() => this.setState({ hover: i })} onMouseLeave={() => this.setState({ hover: null })} onMouseDown={ this.state.editing ? (e) => this._handleMouseDown(e, i) : () => {} } // onClick={this.state.editing ? () => {} : () => this.props.onSelect(i)} style={{ top: pos.y * unit, left: pos.x * unit, width: pos.w * unit, height: this.state.fileNames ? (pos.h + TAG_HEIGHT) * unit : pos.h * unit, zIndex: pos.z, boxShadow: this.state.dragIndex === i ? `0 0 44px 0 rgba(0, 0, 0, 0.25)` : null, backgroundColor: Constants.system.white, }} > {numChecked || this.state.hover === i ? (
{ this._stopProp(e); let checked = this.state.checked; if (checked[i]) { delete checked[i]; } else { checked[i] = true; } this.setState({ checked }); }} >
{this.state.hover !== i ? null : this.state.editing ? ( {this.state.tooltip && this.state.tooltip.startsWith(`${i}-`) ? ( {this.state.tooltip === `${i}-remove` ? "Remove from collection" : this.state.tooltip === `${i}-view` ? "View file" : this.state.tooltip === `${i}-download` ? "Download" : "Delete file"} ) : null}
this.setState({ tooltip: `${i}-remove` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={(e) => { this._handleRemoveFromSlate(e, i); }} style={{ position: "absolute", top: 8, right: 8, cursor: "pointer", margin: 0, }} css={STYLES_ICON_CIRCLE} >
this.setState({ tooltip: `${i}-view` })} onMouseLeave={() => this.setState({ tooltip: null })} // onClick={(e) => { // this._stopProp(e); // this.props.onSelect(i); // }} >
this.setState({ tooltip: `${i}-download` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={(e) => { this._handleDownload(e, i); }} >
this.setState({ tooltip: `${i}-delete` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={ this.state.items[i].ownerId === this.props.viewer.id ? () => { this.setState({ modalShowDeleteFiles: true }); } : () => {} } style={{ cursor: this.state.items[i].ownerId === this.props.viewer.id ? "pointer" : "not-allowed", }} >
) : ( {this.state.tooltip && this.state.tooltip.startsWith(`${i}-`) ? ( {this.state.tooltip === `${i}-add` ? "Add to collection" : this.state.tooltip === `${i}-copy` ? "Copy link" : this.state.tooltip === `${i}-download` ? "Download" : this.state.tooltip === `${i}-preview` ? "Make cover image" : this.state.tooltip === `${i}-remove` ? "Remove from collection" : "Save"} ) : null} {this.props.isOwner ? (
this.setState({ tooltip: `${i}-remove` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={ this.props.external ? this._handleLoginModal : (e) => { this._handleRemoveFromSlate(e, i); } } style={{ position: "absolute", top: 8, right: 8, cursor: "pointer", margin: 0, }} css={STYLES_ICON_CIRCLE} >
) : (
this.setState({ tooltip: `${i}-add` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={ this.props.external ? this._handleLoginModal : (e) => { this._handleAddToSlate(e, i); } } style={{ position: "absolute", top: 8, right: 8, cursor: "pointer", margin: 0, }} css={STYLES_ICON_CIRCLE} >
)}
this.setState({ tooltip: `${i}-download` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={ this.props.external ? this._handleLoginModal : (e) => { this._handleDownload(e, i); } } >
{this.props.isOwner ? (
this.setState({ tooltip: `${i}-preview` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={ this.props.external ? this._handleLoginModal : this.state.items[i].data.type && Validations.isPreviewableImage( this.state.items[i].data.type ) && this.state.items[i].data.size && this.state.items[i].data.size < Constants.linkPreviewSizeLimit ? (e) => this._handleSetPreview(e, i) : () => {} } style={ this.props.preview === Strings.getURLfromCID(this.state.items[i].cid) ? { backgroundColor: "rgba(0, 97, 187, 0.75)", } : this.state.items[i].data.type && Validations.isPreviewableImage( this.state.items[i].data.type ) && this.state.items[i].data.size && this.state.items[i].data.size < Constants.linkPreviewSizeLimit ? {} : { color: "#999999", cursor: "not-allowed", } } > {this.props.preview === Strings.getURLfromCID(this.state.items[i].cid) ? ( ) : ( )}
) : (
this.setState({ tooltip: `${i}-save` })} onMouseLeave={() => this.setState({ tooltip: null })} onClick={ this.props.external ? this._handleLoginModal : (e) => this._handleSaveCopy(e, i) } >
)}
)}
) : null} {this.state.fileNames ? (
{this.state.items[i].data.name || this.state.items[i].filename} {Strings.getFileExtension(this.state.items[i].filename)}
) : null} {this.state.editing ? (
this._handleMouseDownResize(e, i)} style={{ display: this.state.hover === i || this.state.dragIndex === i ? "block" : "none", }} >
) : null}
)) ) : (
)}
{this.state.modalShowDeleteFiles && ( )} {numChecked ? (
{numChecked} file{numChecked > 1 ? "s" : ""} selected
{this.props.isOwner ? (
Add to collection Download Remove {Strings.pluralize("Delete file", numChecked)}
this.setState({ checked: {} })}>
) : (
Add to collection Save
this.setState({ checked: {} })}>
)}
) : null} { this._input = c; }} readOnly value={this.state.copyValue} css={STYLES_COPY_INPUT} />
); } }