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 Window from "~/common/window"; import * as Validations from "~/common/validations"; import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview"; import { CheckBox } from "~/components/system/components/CheckBox"; import { css } from "@emotion/core"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { Toggle } from "~/components/system/components/Toggle"; import { dispatchCustomEvent } from "~/common/custom-events"; import { DynamicIcon } from "~/components/core/DynamicIcon"; import { Tooltip } from "~/components/core/Tooltip"; import { ButtonPrimary, ButtonSecondary, ButtonDisabled, ButtonWarning, } from "~/components/system/components/Buttons"; //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 SIZE_LIMIT = 1000000; //NOTE(martina): 1mb limit for twitter preview images 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: null, z: 0, id: item.id, }; }) || [] ); }; const preload = (item) => new Promise((resolve, reject) => { if (!item.type || !Validations.isPreviewableImage(this.props.type)) { resolve(200); } const img = new Image(); img.onload = () => { resolve((200 * img.height) / img.width); }; img.onerror = reject; const url = item.url.replace("https://undefined", "https://"); 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 rgba(229, 229, 229, 0.5); 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.darkGray} 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: -11px; 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.grayBlack}; 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.darkGray}; text-transform: uppercase; flex-shrink: 0; margin-left: 16px; text-align: right; `; const STYLES_HANDLE_BOX = css` cursor: nwse-resize; position: absolute; height: 24px; width: 24px; color: rgba(195, 195, 196, 0.75); `; const STYLES_ACTION_BAR = css` display: flex; align-items: center; justify-content: space-between; box-shadow: 0 0 0 1px ${Constants.system.lightBorder} inset, 0 0 4px 2px ${Constants.system.shadow}; border-radius: 4px; padding: 12px 32px; box-sizing: border-box; background-color: ${Constants.system.foreground}; position: fixed; bottom: 12px; width: calc(100vw - ${Constants.sizes.sidebar}px + 32px); max-width: 1660px; z-index: ${Constants.zindex.header}; @media (max-width: ${Constants.sizes.mobile}px) { width: calc(100vw - 48px); } `; 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}; @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% - 60px); `; 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, }; //LEFT OFF HERE: //make sure things work if you delete while in editing mode (haven't tested that yet) //add the "delete" and checkbox actions. drag handle. and make preview image //if there's repeats of ids (not cids), it'll style them the same way (potentially diff position though) everytime you change something //if width under a certain amount, set to a default mobile flex or grid layout of single items //add flag for following defaultLayout for last element or not. so know where to put next one. "unchanged since last add" flag. if something deleted, messes this up as well componentDidMount = async () => { this.debounceInstance = this.debounce(this._recalculate, 250); window.addEventListener("resize", this.debounceInstance); window.addEventListener("remote-object-update", this._handleRemoteEditObject); 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); window.removeEventListener("remote-object-update", this._handleRemoteEditObject); if (this.state.editing) { window.removeEventListener("keydown", this._handleKeyDown); window.removeEventListener("keyup", this._handleKeyUp); } }; componentDidUpdate = async (prevProps) => { if (prevProps.slateId !== this.props.slateId) { //NOTE(martina): to handle when you navigate between two slates, so it registers the change properly await this.setState({ show: false }); let defaultLayout = this.props.layout ? this.props.defaultLayout : true; let fileNames = this.props.fileNames; let layout; if (this.props.layout) { layout = await this.repairLayout(this.props.items, { defaultLayout, fileNames, layout: this.props.layout, }); if (layout) { this.props.onSaveLayout( { ver: "2.0", fileNames, defaultLayout, layout, }, true ); } else { layout = this.props.layout; } } else { layout = generateLayout(this.props.items); await this.setState({ layout, items: this.props.items }); layout = await this.calculateLayout(layout); this.props.onSaveLayout( { ver: "2.0", fileNames, defaultLayout, layout, }, true ); } await this.setState({ items: this.props.items, layout, prevLayouts: [], zIndexMax: layout && layout.length ? Math.max(...layout.map((pos) => pos.z)) + 1 : 1, fileNames, defaultLayout, editing: false, show: true, }); this.calculateContainer(); } else 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 ); } } } }; _handleRemoteEditObject = async ({ detail }) => { const { object } = detail; const items = [...this.state.items]; for (let i = 0; i < items.length; i++) { if (items[i].id === object.id) { items[i] = object; break; } } this.setState({ items }); }; debounce = (func, ms) => { let timer; return () => { window.clearTimeout(timer); timer = window.setTimeout(func, ms); }; }; _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); let repairNeeded = false; if (items.length === layout.length) { let itemIds = items.map((item) => item.id); for (let i = 0; i < itemIds.length; i++) { if (itemIds[i] !== layoutIds[i]) { repairNeeded = true; break; } } if (!repairNeeded) { return; } } let newLayout = items.map((item) => null) || []; for (let i = 0; i < items.length; i++) { let layoutIndex = layoutIds.indexOf(items[i].id); if (layoutIndex === -1) { 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; } } 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, }; 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]; if (height === 0) continue; 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, }; } return layout; }; calculateHeights = async () => { let results = await Promise.allSettled(this.state.items.map((item, i) => preload(item, i))); let heights = results.map((result) => { if (result.status === "fulfilled") { return result.value; } 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), }, ], }); } }; _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(); this._handleHelpKeybind(); } }; _handleKeyUp = (e) => { this.keysPressed[e.key] = false; if (!this.keysPressed["Meta"] || !this.keysPressed["Control"]) { this._handleHelpKeybind(); } this.keysPressed = {}; }; _handleHelpKeybind = () => { if (!this.state.keyboardTooltip) { this.setState({ keyboardTooltip: true }); } else { this.setState({ keyboardTooltip: false }); } }; _handleUndo = () => { if (this.state.prevLayouts.length) { let prevLayouts = this.state.prevLayouts; let layouts = prevLayouts.pop(); this.setState({ ...layouts, prevLayouts }); } }; _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 }); }; _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 () => { if ( !window.confirm("Are you sure you want to reset your layout to the default column layout?") ) { 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, }); }; _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 }, }); }; _handleDelete = (ids) => { dispatchCustomEvent({ name: "remote-delete-object", detail: { ids } }); }; _handleCopy = (e, value) => { e.stopPropagation(); e.preventDefault(); this.setState({ copyValue: value }, () => { this._input.select(); document.execCommand("copy"); }); }; _handleLoginModal = (e) => { e.preventDefault(); e.stopPropagation(); //TODO(martina): add a modal popup that says "login or sign up to use this feature", and automatically redirect to in-client view if already logged in window.location.pathname = "/_"; }; _handleSetPreview = (e, i) => { e.stopPropagation(); e.preventDefault(); let url = this.state.items[i].url.replace("https://undefined", "https://"); if (this.props.preview === url) return; this.props.onSavePreview(url); }; _handleDownload = (e, i) => { e.stopPropagation(); e.preventDefault(); if (i !== undefined) { let filename = this.state.items[i].name || this.state.items[i].title; let uri = this.state.items[i].url.replace("https://undefined", "https://"); Window.saveAs(uri, filename); } // else { // for (let i of Object.keys(this.state.checked)) { // console.log(i); // download = this.state.items[i].name || this.state.items[i].title; // uri = this.state.items[i].url.replace("https://undefined", "https://"); // let link = document.createElement("a"); // document.body.appendChild(link); // link.target = "_blank"; // link.download = download; // link.href = uri; // link.click(); // document.body.removeChild(link); // } // } // this.setState({ checked: {} }); }; _handleSaveCopy = (e, i) => { e.stopPropagation(); e.preventDefault(); let items = []; if (i !== undefined) { items = [ { ownerId: this.state.items[i].ownerId, cid: this.state.items[i].cid || Strings.urlToCid(this.state.items[i].url), }, ]; } else { for (let i of Object.keys(this.state.checked)) { items.push({ ownerId: this.state.items[i].ownerId, cid: this.state.items[i].cid || Strings.urlToCid(this.state.items[i].url), }); } } this.props.onSaveCopy(items); this.setState({ checked: {} }); }; _handleAddToSlate = (e, i) => { 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]; } else { for (let index of Object.keys(this.state.checked)) { ids.push(this.state.items[index].id); } } this.props.onRemoveFromSlate({ detail: { ids } }); this.setState({ checked: {} }); }; _handleDeleteFiles = (e, i) => { e.stopPropagation(); e.preventDefault(); let ids = []; if (i !== undefined) { ids = [this.state.items[i].id]; } else { for (let index of Object.keys(this.state.checked)) { ids.push(this.state.items[index].id); } } let cids = []; for (let file of this.props.viewer.library[0].children) { if (ids.includes(file.id)) { cids.push(file.cid || file.ipfs.replace("/ipfs/", "")); } } this.props.onDeleteFiles(cids); this.setState({ checked: {} }); }; _stopProp = (e) => { e.stopPropagation(); e.preventDefault(); }; render() { let numChecked = Object.keys(this.state.checked).length; let unit = this.state.unit; return (
Keyboard shortcuts
shift + click and drag
keep x value or y value while moving file
shift + click and resize
keep aspect ratio while resizing
ctrl + click and drag
move without snapping to the dot grid
ctrl + click and resize
resize without snapping to the dot grid