More mobile tweaks (#790)

* Disable text selection

* Set content-editable=plaintext-only to disable Touch Bar formatting buttons

* Enlarge resize handle tap targets for pen/touch

* Make the lock button a button in mobile mode

* Use icons instead of Unicode characters; add an alternate toolbar for creating multipoint lines

* Allow buttons to hide themselves

* Fix heuristic for showing shape actions

* Refactor icons

* Fix label for edit button

* Switch edit button icon

* Remove lock button on mobile

* Add language selector on mobile

* Fix showing edit button on mobile

* Fix showing edit button on mobile, part 2

* Fix handle touch regions

* Fix scroll-back button position

* Allow using the text tool on a text object to start editing it

* Fix deletion of last point in line
This commit is contained in:
Jed Fox 2020-02-21 14:34:18 -05:00 committed by GitHub
parent 949c3841ea
commit 0fd3fb4b5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 272 additions and 222 deletions

View File

@ -18,13 +18,14 @@ export const actionDeleteSelected: Action = {
contextMenuOrder: 3,
commitToHistory: (_, elements) => isSomeElementSelected(elements),
keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
PanelComponent: ({ updateData }) => (
PanelComponent: ({ elements, updateData }) => (
<ToolButton
type="button"
icon={trash}
title={t("labels.delete")}
aria-label={t("labels.delete")}
onClick={() => updateData(null)}
visible={isSomeElementSelected(elements)}
/>
),
};

View File

@ -5,7 +5,7 @@ import { isInvisiblySmallElement } from "../element";
import { resetCursor } from "../utils";
import React from "react";
import { ToolButton } from "../components/ToolButton";
import { save } from "../components/icons";
import { done } from "../components/icons";
import { t } from "../i18n";
export const actionFinalize: Action = {
@ -16,10 +16,13 @@ export const actionFinalize: Action = {
window.document.activeElement.blur();
}
if (appState.multiElement) {
// pen and mouse have hover
if (appState.lastPointerDownWith !== "touch") {
appState.multiElement.points = appState.multiElement.points.slice(
0,
appState.multiElement.points.length - 1,
);
}
if (isInvisiblySmallElement(appState.multiElement)) {
newElements = newElements.slice(0, -1);
}
@ -50,12 +53,12 @@ export const actionFinalize: Action = {
PanelComponent: ({ appState, updateData }) => (
<div
style={{
visibility: appState.multiElement !== null ? "visible" : "hidden",
visibility: appState.multiElement != null ? "visible" : "hidden",
}}
>
<ToolButton
type="button"
icon={save}
icon={done}
title={t("buttons.done")}
aria-label={t("buttons.done")}
onClick={() => updateData(null)}

View File

@ -30,6 +30,7 @@ export function getDefaultAppState(): AppState {
selectionElement: null,
zoom: 1,
openedMenu: null,
lastPointerDownWith: "mouse",
};
}

View File

@ -5,15 +5,19 @@ export function LanguageList<T>({
onChange,
languages,
currentLanguage,
floating,
}: {
languages: { lng: string; label: string }[];
onChange: (value: string) => void;
currentLanguage: string;
floating?: boolean;
}) {
return (
<React.Fragment>
<select
className="language-select"
className={`dropdown-select dropdown-select__language${
floating ? " dropdown-select--floating" : ""
}`}
onChange={({ target }) => onChange(target.value)}
value={currentLanguage}
aria-label={t("buttons.selectLanguage")}

View File

@ -11,6 +11,7 @@ type LockIconProps = {
checked: boolean;
onChange?(): void;
size?: LockIconSize;
isButton?: boolean;
};
const DEFAULT_SIZE: LockIconSize = "m";
@ -43,7 +44,12 @@ export function LockIcon(props: LockIconProps) {
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
return (
<label className={`ToolIcon ToolIcon__lock ${sizeCn}`} title={props.title}>
<label
className={`ToolIcon ToolIcon__lock ${
props.isButton ? "ToolIcon_type_button" : "ToolIcon_type_floating"
} ${sizeCn}`}
title={props.title}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"

View File

@ -36,7 +36,7 @@ export class ProjectName extends Component<Props> {
return (
<span
suppressContentEditableWarning
contentEditable="true"
contentEditable={"plaintext-only" as any}
data-type="wysiwyg"
className="ProjectName"
role="textbox"

View File

@ -15,6 +15,7 @@ type ToolButtonBaseProps = {
size?: ToolIconSize;
keyBindingLabel?: string;
showAriaLabel?: boolean;
visible?: boolean;
};
type ToolButtonProps =
@ -45,6 +46,10 @@ export const ToolButton = React.forwardRef(function(
type="button"
onClick={props.onClick}
ref={innerRef}
style={{
visibility:
props.visible || props.visible == null ? "visible" : "hidden",
}}
>
<div className="ToolIcon__icon" aria-hidden="true">
{props.icon || props.label}

View File

@ -5,6 +5,7 @@
font-family: Cascadia;
cursor: pointer;
background-color: #e9ecef;
-webkit-tap-highlight-color: transparent;
}
.ToolIcon__icon {
@ -69,17 +70,8 @@
}
}
.ToolIcon.ToolIcon__lock {
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.1rem;
.ToolIcon_type_floating {
background-color: transparent;
.ToolIcon__icon {
width: 2rem;
height: 2em;
}
&:hover {
background-color: transparent;
}
@ -89,6 +81,22 @@
&:focus {
box-shadow: none;
}
.ToolIcon__icon {
width: 2rem;
height: 2em;
}
}
.ToolIcon.ToolIcon__lock {
&.ToolIcon_type_button {
border-radius: 4px;
svg {
position: static;
}
}
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__keybinding {

View File

@ -5,92 +5,77 @@
import React from "react";
export const link = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 512 512">
<path
fill="currentColor"
d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"
/>
const createIcon = (d: string, width = 512) => (
<svg
aria-hidden="true"
focusable="false"
role="img"
viewBox={`0 0 ${width} 512`}
>
<path fill="currentColor" d={d} />
</svg>
);
export const save = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
/>
</svg>
export const link = createIcon(
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
);
export const load = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 576 512">
<path
fill="currentColor"
d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
/>
</svg>
export const save = createIcon(
"M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z",
448,
);
export const image = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 384 512">
<path
fill="currentColor"
d="M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z"
/>
</svg>
export const load = createIcon(
"M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z",
576,
);
export const clipboard = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 384 512">
<path
fill="currentColor"
d="M384 112v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h80c0-35.29 28.71-64 64-64s64 28.71 64 64h80c26.51 0 48 21.49 48 48zM192 40c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24m96 114v-20a6 6 0 0 0-6-6H102a6 6 0 0 0-6 6v20a6 6 0 0 0 6 6h180a6 6 0 0 0 6-6z"
/>
</svg>
export const image = createIcon(
"M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z",
384,
);
export const trash = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
/>
</svg>
export const clipboard = createIcon(
"M384 112v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h80c0-35.29 28.71-64 64-64s64 28.71 64 64h80c26.51 0 48 21.49 48 48zM192 40c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24m96 114v-20a6 6 0 0 0-6-6H102a6 6 0 0 0-6 6v20a6 6 0 0 0 6 6h180a6 6 0 0 0 6-6z",
384,
);
export const palete = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 512 512">
<path
fill="currentColor"
d="M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"
/>
</svg>
export const trash = createIcon(
"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z",
448,
);
export const exportFile = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 576 512">
<path
fill="currentColor"
d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z"
/>
</svg>
export const palette = createIcon(
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",
);
export const zoomIn = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"
/>
</svg>
export const exportFile = createIcon(
"M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z",
576,
);
export const zoomOut = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"
/>
</svg>
export const zoomIn = createIcon(
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
448,
);
export const zoomOut = createIcon(
"M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
448,
);
export const done = createIcon(
"M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z",
);
export const menu = createIcon(
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z",
);
export const undo = createIcon(
"M255.545 8c-66.269.119-126.438 26.233-170.86 68.685L48.971 40.971C33.851 25.851 8 36.559 8 57.941V192c0 13.255 10.745 24 24 24h134.059c21.382 0 32.09-25.851 16.971-40.971l-41.75-41.75c30.864-28.899 70.801-44.907 113.23-45.273 92.398-.798 170.283 73.977 169.484 169.442C423.236 348.009 349.816 424 256 424c-41.127 0-79.997-14.678-110.63-41.556-4.743-4.161-11.906-3.908-16.368.553L89.34 422.659c-4.872 4.872-4.631 12.815.482 17.433C133.798 479.813 192.074 504 256 504c136.966 0 247.999-111.033 248-247.998C504.001 119.193 392.354 7.755 255.545 8z",
);
export const redo = createIcon(
"M256.455 8c66.269.119 126.437 26.233 170.859 68.685l35.715-35.715C478.149 25.851 504 36.559 504 57.941V192c0 13.255-10.745 24-24 24H345.941c-21.382 0-32.09-25.851-16.971-40.971l41.75-41.75c-30.864-28.899-70.801-44.907-113.23-45.273-92.398-.798-170.283 73.977-169.484 169.442C88.764 348.009 162.184 424 256 424c41.127 0 79.997-14.678 110.629-41.556 4.743-4.161 11.906-3.908 16.368.553l39.662 39.662c4.872 4.872 4.631 12.815-.482 17.433C378.202 479.813 319.926 504 256 504 119.034 504 8.001 392.967 8 256.002 7.999 119.193 119.646 7.755 256.455 8z",
);

View File

@ -1,15 +1,26 @@
import { ExcalidrawElement } from "./types";
import { ExcalidrawElement, PointerType } from "./types";
import { getElementAbsoluteCoords } from "./bounds";
type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se";
export function handlerRectangles(element: ExcalidrawElement, zoom: number) {
const handlerWidth = 8 / zoom;
const handlerHeight = 8 / zoom;
const handleSizes: { [k in PointerType]: number } = {
mouse: 8,
pen: 16,
touch: 28,
};
const handlerMarginX = 8 / zoom;
const handlerMarginY = 8 / zoom;
export function handlerRectangles(
element: ExcalidrawElement,
zoom: number,
pointerType: PointerType = "mouse",
) {
const size = handleSizes[pointerType];
const handlerWidth = size / zoom;
const handlerHeight = size / zoom;
const handlerMarginX = size / zoom;
const handlerMarginY = size / zoom;
const [elementX1, elementY1, elementX2, elementY2] = getElementAbsoluteCoords(
element,
@ -20,59 +31,61 @@ export function handlerRectangles(element: ExcalidrawElement, zoom: number) {
const dashedLineMargin = 4 / zoom;
const centeringOffset = (size - 8) / (2 * zoom);
const handlers = {
nw: [
elementX1 - dashedLineMargin - handlerMarginX,
elementY1 - dashedLineMargin - handlerMarginY,
elementX1 - dashedLineMargin - handlerMarginX + centeringOffset,
elementY1 - dashedLineMargin - handlerMarginY + centeringOffset,
handlerWidth,
handlerHeight,
],
ne: [
elementX2 + dashedLineMargin,
elementY1 - dashedLineMargin - handlerMarginY,
elementX2 + dashedLineMargin - centeringOffset,
elementY1 - dashedLineMargin - handlerMarginY + centeringOffset,
handlerWidth,
handlerHeight,
],
sw: [
elementX1 - dashedLineMargin - handlerMarginX,
elementY2 + dashedLineMargin,
elementX1 - dashedLineMargin - handlerMarginX + centeringOffset,
elementY2 + dashedLineMargin - centeringOffset,
handlerWidth,
handlerHeight,
],
se: [
elementX2 + dashedLineMargin,
elementY2 + dashedLineMargin,
elementX2 + dashedLineMargin - centeringOffset,
elementY2 + dashedLineMargin - centeringOffset,
handlerWidth,
handlerHeight,
],
} as { [T in Sides]: number[] };
// We only want to show height handlers (all cardinal directions) above a certain size
const minimumSizeForEightHandlers = 40 / zoom;
const minimumSizeForEightHandlers = (5 * size) / zoom;
if (Math.abs(elementWidth) > minimumSizeForEightHandlers) {
handlers["n"] = [
elementX1 + elementWidth / 2,
elementY1 - dashedLineMargin - handlerMarginY,
elementX1 + elementWidth / 2 - handlerWidth / 2,
elementY1 - dashedLineMargin - handlerMarginY + centeringOffset,
handlerWidth,
handlerHeight,
];
handlers["s"] = [
elementX1 + elementWidth / 2,
elementY2 + dashedLineMargin,
elementX1 + elementWidth / 2 - handlerWidth / 2,
elementY2 + dashedLineMargin - centeringOffset,
handlerWidth,
handlerHeight,
];
}
if (Math.abs(elementHeight) > minimumSizeForEightHandlers) {
handlers["w"] = [
elementX1 - dashedLineMargin - handlerMarginX,
elementY1 + elementHeight / 2,
elementX1 - dashedLineMargin - handlerMarginX + centeringOffset,
elementY1 + elementHeight / 2 - handlerHeight / 2,
handlerWidth,
handlerHeight,
];
handlers["e"] = [
elementX2 + dashedLineMargin,
elementY1 + elementHeight / 2,
elementX2 + dashedLineMargin - centeringOffset,
elementY1 + elementHeight / 2 - handlerHeight / 2,
handlerWidth,
handlerHeight,
];

View File

@ -1,4 +1,4 @@
import { ExcalidrawElement } from "./types";
import { ExcalidrawElement, PointerType } from "./types";
import { handlerRectangles } from "./handlerRectangles";
@ -9,12 +9,13 @@ export function resizeTest(
x: number,
y: number,
zoom: number,
pointerType: PointerType,
): HandlerRectanglesRet | false {
if (!element.isSelected || element.type === "text") {
return false;
}
const handlers = handlerRectangles(element, zoom);
const handlers = handlerRectangles(element, zoom, pointerType);
const filter = Object.keys(handlers).filter(key => {
const handler = handlers[key as HandlerRectanglesRet]!;
@ -41,12 +42,13 @@ export function getElementWithResizeHandler(
elements: readonly ExcalidrawElement[],
{ x, y }: { x: number; y: number },
zoom: number,
pointerType: PointerType,
) {
return elements.reduce((result, element) => {
if (result) {
return result;
}
const resizeHandle = resizeTest(element, x, y, zoom);
const resizeHandle = resizeTest(element, x, y, zoom, pointerType);
return resizeHandle ? { element, resizeHandle } : null;
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
}

View File

@ -34,7 +34,7 @@ export function textWysiwyg({
// But this solution has an issue — it allows to paste
// multiline text, which is not currently supported
const editable = document.createElement("div");
editable.contentEditable = "true";
editable.contentEditable = "plaintext-only";
editable.tabIndex = 0;
editable.innerText = initText;
editable.dataset.type = "wysiwyg";

View File

@ -9,3 +9,5 @@ export type ExcalidrawTextElement = ExcalidrawElement & {
actualBoundingBoxAscent?: number;
baseline: number;
};
export type PointerType = "mouse" | "pen" | "touch";

View File

@ -109,6 +109,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
import { normalizeScroll } from "./scene/data";
import { getCenter, getDistance } from "./gesture";
import { menu, palette } from "./components/icons";
let { elements } = createScene();
const { history } = createHistory();
@ -286,9 +287,11 @@ const LayerUI = React.memo(
);
}
const showSelectedShapeActions =
(appState.editingElement || getSelectedElements(elements).length) &&
appState.elementType === "selection";
const showSelectedShapeActions = Boolean(
appState.editingElement ||
getSelectedElements(elements).length ||
appState.elementType !== "selection",
);
function renderSelectedShapeActions() {
const { elementType, editingElement } = appState;
@ -386,21 +389,6 @@ const LayerUI = React.memo(
);
}
const lockButton = (
<LockIcon
checked={appState.elementLocked}
onChange={() => {
setAppState({
elementLocked: !appState.elementLocked,
elementType: appState.elementLocked
? "selection"
: appState.elementType,
});
}}
title={t("toolBar.lock")}
/>
);
return isMobile ? (
<>
{appState.openedMenu === "canvas" ? (
@ -411,13 +399,24 @@ const LayerUI = React.memo(
<h2 className="visually-hidden" id="canvas-actions-title">
{t("headings.canvasActions")}
</h2>
<div className="App-mobile-menu-scroller">
<div className="App-mobile-menu-scroller panelColumn">
<Stack.Col gap={4}>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{renderExportDialog()}
{actionManager.renderAction("clearCanvas")}
{actionManager.renderAction("changeViewBackgroundColor")}
<fieldset>
<legend>{t("labels.language")}</legend>
<LanguageList
onChange={lng => {
setLanguage(lng);
setAppState({});
}}
languages={languages}
currentLanguage={language}
/>
</fieldset>
</Stack.Col>
</div>
</section>
@ -456,11 +455,27 @@ const LayerUI = React.memo(
</FixedSideContainer>
<footer className="App-toolbar">
<div className="App-toolbar-content">
{appState.multiElement ? (
<>
{actionManager.renderAction("deleteSelectedElements")}
<ToolButton
visible={showSelectedShapeActions}
type="button"
icon={palette}
aria-label={t("buttons.edit")}
onClick={() =>
setAppState(({ openedMenu }: any) => ({
openedMenu: openedMenu === "shape" ? null : "shape",
}))
}
/>
{actionManager.renderAction("finalize")}
</>
) : (
<>
<ToolButton
type="button"
icon={
<span style={{ fontSize: "2em", marginTop: "-0.15em" }}></span>
}
icon={menu}
aria-label={t("buttons.menu")}
onClick={() =>
setAppState(({ openedMenu }: any) => ({
@ -468,40 +483,18 @@ const LayerUI = React.memo(
}))
}
/>
<div
style={{
visibility: isSomeElementSelected(elements)
? "visible"
: "hidden",
}}
>
{" "}
{actionManager.renderAction("deleteSelectedElements")}
</div>
{lockButton}
{actionManager.renderAction("finalize")}
<div
style={{
visibility: isSomeElementSelected(elements)
? "visible"
: "hidden",
}}
>
<ToolButton
visible={showSelectedShapeActions}
type="button"
icon={
<span style={{ fontSize: "2em", marginTop: "-0.15em" }}>
</span>
}
aria-label={t("buttons.menu")}
icon={palette}
aria-label={t("buttons.edit")}
onClick={() =>
setAppState(({ openedMenu }: any) => ({
openedMenu: openedMenu === "shape" ? null : "shape",
}))
}
/>
</div>
{actionManager.renderAction("deleteSelectedElements")}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
@ -512,6 +505,8 @@ const LayerUI = React.memo(
{t("buttons.scrollBackToContent")}
</button>
)}
</>
)}
</div>
</footer>
</>
@ -545,7 +540,7 @@ const LayerUI = React.memo(
</Stack.Col>
</Island>
</section>
{showSelectedShapeActions ? (
{showSelectedShapeActions && (
<section
className="App-right-menu"
aria-labelledby="selected-shape-title"
@ -555,7 +550,7 @@ const LayerUI = React.memo(
</h2>
<Island padding={4}>{renderSelectedShapeActions()}</Island>
</section>
) : null}
)}
</Stack.Col>
<section aria-labelledby="shapes-title">
<Stack.Col gap={4} align="start">
@ -566,7 +561,19 @@ const LayerUI = React.memo(
</h2>
<Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
</Island>
{lockButton}
<LockIcon
checked={appState.elementLocked}
onChange={() => {
setAppState({
elementLocked: !appState.elementLocked,
elementType: appState.elementLocked
? "selection"
: appState.elementType,
});
}}
title={t("toolBar.lock")}
isButton={isMobile}
/>
</Stack.Row>
</Stack.Col>
</section>
@ -591,6 +598,7 @@ const LayerUI = React.memo(
}}
languages={languages}
currentLanguage={language}
floating
/>
{appState.scrolledOutside && (
<button
@ -1085,6 +1093,8 @@ export class App extends React.Component<any, AppState> {
return;
}
this.setState({ lastPointerDownWith: e.pointerType });
// pan canvas on wheel button drag or space+drag
if (
gesture.pointers.length === 0 &&
@ -1213,6 +1223,7 @@ export class App extends React.Component<any, AppState> {
elements,
{ x, y },
this.state.zoom,
e.pointerType,
);
const selectedElements = getSelectedElements(elements);
@ -1279,6 +1290,9 @@ export class App extends React.Component<any, AppState> {
if (this.state.editingElement?.type === "text") {
return;
}
if (elementIsAddedToSelection) {
element = hitElement!;
}
let textX = e.clientX;
let textY = e.clientY;
if (!e.altKey) {
@ -2152,6 +2166,7 @@ export class App extends React.Component<any, AppState> {
elements,
{ x, y },
this.state.zoom,
e.pointerType,
);
if (resizeElement && resizeElement.resizeHandle) {
document.documentElement.style.cursor = getCursorForResizingElement(

View File

@ -40,7 +40,8 @@
"colorPicker": "Color picker",
"canvasBackground": "Canvas background",
"drawingCanvas": "Drawing Canvas",
"layers": "Layers"
"layers": "Layers",
"language": "Language"
},
"buttons": {
"clearReset": "Reset the canvas",
@ -57,7 +58,8 @@
"zoomIn": "Zoom in",
"zoomOut": "Zoom out",
"menu": "Menu",
"done": "Done"
"done": "Done",
"edit": "Edit"
},
"alerts": {
"clearReset": "This will clear the whole canvas. Are you sure?",

View File

@ -6,6 +6,11 @@ body {
font-family: var(--ui-font);
color: var(--text-color-primary);
-webkit-text-size-adjust: 100%;
user-select: none;
}
[contenteditable] {
user-select: auto;
cursor: text;
}
canvas {
@ -139,11 +144,19 @@ button,
}
}
.App-toolbar,
.App-mobile-menu {
--spacing: 0.5rem;
--padding: calc(4 * var(--space-factor));
padding: var(--padding);
padding-left: #{"max(var(--padding), env(safe-area-inset-left))"};
padding-right: #{"max(var(--padding), env(safe-area-inset-right))"};
background: #fcfcfc;
border-top: 1px solid #ccc;
box-sizing: border-box;
}
.App-toolbar {
padding: var(--spacing);
padding-bottom: #{"max(var(--spacing), env(safe-area-inset-bottom))"};
padding-left: #{"max(var(--spacing), env(safe-area-inset-left))"};
padding-right: #{"max(var(--spacing), env(safe-area-inset-right))"};
padding-bottom: #{"max(var(--padding), env(safe-area-inset-bottom))"};
width: 100%;
box-sizing: border-box;
overflow: auto;
@ -155,14 +168,8 @@ button,
align-items: center;
justify-content: space-between;
}
.App-toolbar,
.App-mobile-menu {
--spacing: 0.5rem;
background: #fcfcfc;
border-top: 1px solid #ccc;
}
.App-mobile-menu {
--bottom: calc(3rem - 1px + max(var(--spacing), env(safe-area-inset-bottom)));
--bottom: calc(3rem - 1px + max(var(--padding), env(safe-area-inset-bottom)));
display: grid;
position: fixed;
width: 100%;
@ -174,10 +181,6 @@ button,
.App-mobile-menu .App-mobile-menu-scroller {
background: #fcfcfc;
box-shadow: none;
--padding: calc(4 * var(--space-factor));
padding: var(--padding);
padding-left: #{"max(var(--padding), env(safe-area-inset-left))"};
padding-right: #{"max(var(--padding), env(safe-area-inset-right))"};
}
.App-menu {
@ -288,12 +291,7 @@ button,
}
.dropdown-select {
position: absolute;
margin-bottom: 0.5em;
margin-right: 0.5em;
height: 1.5rem;
right: 0;
bottom: 0;
padding: 0 1.5rem 0 0.5rem;
background-color: #e9ecef;
border-radius: var(--space-factor);
@ -317,10 +315,14 @@ button,
&:active {
background-color: #ced4da;
}
&.dropdown-select--floating {
position: absolute;
margin-bottom: 0.5em;
margin-right: 0.5em;
}
}
.language-select {
@extend .dropdown-select;
.dropdown-select__language.dropdown-select--floating {
right: 0;
bottom: 0;
}
@ -351,7 +353,7 @@ button,
.scroll-back-to-content {
position: fixed;
left: 50%;
bottom: 20px;
bottom: 30px;
transform: translateX(-50%);
padding: 10px 20px;
}
@ -361,7 +363,7 @@ button,
display: none;
}
.scroll-back-to-content {
bottom: 70px;
bottom: calc(70px + env(safe-area-inset-bottom));
bottom: 80px;
bottom: calc(80px + env(safe-area-inset-bottom));
}
}

View File

@ -1,4 +1,4 @@
import { ExcalidrawElement } from "./element/types";
import { ExcalidrawElement, PointerType } from "./element/types";
import { SHAPES } from "./shapes";
export type FlooredNumber = number & { _brand: "FlooredNumber" };
@ -32,6 +32,7 @@ export type AppState = {
isResizing: boolean;
zoom: number;
openedMenu: "canvas" | "shape" | null;
lastPointerDownWith: PointerType;
};
export type Pointer = Readonly<{