mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 01:06:35 +00:00
Allow to move and hide sidebar items (#6755)
This commit is contained in:
parent
5292119e6e
commit
3bd2e8dbf5
@ -114,6 +114,7 @@
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sortablejs": "^1.10.2",
|
||||
"superstruct": "^0.10.12",
|
||||
"unfetch": "^4.1.0",
|
||||
"vue": "^2.6.11",
|
||||
|
68
src/common/decorators/local-storage.ts
Normal file
68
src/common/decorators/local-storage.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type { ClassElement } from "../../types";
|
||||
|
||||
class Storage {
|
||||
private _storage: any = {};
|
||||
|
||||
public addFromStorage(storageKey: any): void {
|
||||
if (!this._storage[storageKey]) {
|
||||
const data = window.localStorage.getItem(storageKey);
|
||||
if (data) {
|
||||
this._storage[storageKey] = JSON.parse(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public hasKey(storageKey: string): any {
|
||||
return storageKey in this._storage;
|
||||
}
|
||||
|
||||
public getValue(storageKey: string): any {
|
||||
return this._storage[storageKey];
|
||||
}
|
||||
|
||||
public setValue(storageKey: string, value: any): any {
|
||||
this._storage[storageKey] = value;
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
// Safari in private mode doesn't allow localstorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
export const LocalStorage = (key?: string) => {
|
||||
return (element: ClassElement, propName: string) => {
|
||||
const storageKey = key || propName;
|
||||
const initVal = element.initializer ? element.initializer() : undefined;
|
||||
|
||||
storage.addFromStorage(storageKey);
|
||||
|
||||
const getValue = (): any => {
|
||||
return storage.hasKey(storageKey)
|
||||
? storage.getValue(storageKey)
|
||||
: initVal;
|
||||
};
|
||||
|
||||
const setValue = (val: any) => {
|
||||
storage.setValue(storageKey, val);
|
||||
};
|
||||
|
||||
return {
|
||||
kind: "method",
|
||||
placement: "own",
|
||||
key: element.key,
|
||||
descriptor: {
|
||||
set(value) {
|
||||
setValue(value);
|
||||
},
|
||||
get() {
|
||||
return getValue();
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
77
src/components/ha-sidebar-sort-styles.ts
Normal file
77
src/components/ha-sidebar-sort-styles.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { html } from "lit-element";
|
||||
|
||||
export const sortStyles = html`
|
||||
<style>
|
||||
#sortable a:nth-of-type(2n) paper-icon-item {
|
||||
animation-name: keyframes1;
|
||||
animation-iteration-count: infinite;
|
||||
transform-origin: 50% 10%;
|
||||
animation-delay: -0.75s;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
#sortable a:nth-of-type(2n-1) paper-icon-item {
|
||||
animation-name: keyframes2;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
transform-origin: 30% 5%;
|
||||
animation-delay: -0.5s;
|
||||
animation-duration: 0.33s;
|
||||
}
|
||||
|
||||
#sortable {
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes keyframes1 {
|
||||
0% {
|
||||
transform: rotate(-1deg);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(1.5deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes keyframes2 {
|
||||
0% {
|
||||
transform: rotate(1deg);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(-1.5deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
:host([expanded]) .hide-panel {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
paper-icon-item.hidden-panel,
|
||||
paper-icon-item.hidden-panel span,
|
||||
paper-icon-item.hidden-panel ha-icon[slot="item-icon"] {
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
@ -1,9 +1,12 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCellphoneCog,
|
||||
mdiMenuOpen,
|
||||
mdiClose,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
mdiPlus,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
@ -13,20 +16,24 @@ import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
eventOptions,
|
||||
html,
|
||||
customElement,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { guard } from "lit-html/directives/guard";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { LocalStorage } from "../common/decorators/local-storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { compare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
import { ActionHandlerDetail } from "../data/lovelace";
|
||||
import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
@ -35,6 +42,7 @@ import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-menu-button";
|
||||
@ -54,11 +62,39 @@ const SORT_VALUE_URL_PATHS = {
|
||||
config: 11,
|
||||
};
|
||||
|
||||
const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
const panelSorter = (
|
||||
reverseSort: string[],
|
||||
defaultPanel: string,
|
||||
a: PanelInfo,
|
||||
b: PanelInfo
|
||||
) => {
|
||||
const indexA = reverseSort.indexOf(a.url_path);
|
||||
const indexB = reverseSort.indexOf(b.url_path);
|
||||
if (indexA !== indexB) {
|
||||
if (indexA < indexB) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return defaultPanelSorter(defaultPanel, a, b);
|
||||
};
|
||||
|
||||
const defaultPanelSorter = (
|
||||
defaultPanel: string,
|
||||
a: PanelInfo,
|
||||
b: PanelInfo
|
||||
) => {
|
||||
// Put all the Lovelace at the top.
|
||||
const aLovelace = a.component_name === "lovelace";
|
||||
const bLovelace = b.component_name === "lovelace";
|
||||
|
||||
if (a.url_path === defaultPanel) {
|
||||
return -1;
|
||||
}
|
||||
if (b.url_path === defaultPanel) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aLovelace && bLovelace) {
|
||||
return compare(a.title!, b.title!);
|
||||
}
|
||||
@ -85,30 +121,45 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
return compare(a.title!, b.title!);
|
||||
};
|
||||
|
||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const panels = hass.panels;
|
||||
if (!panels) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (!panel.title || panel.url_path === hass.defaultPanel) {
|
||||
return;
|
||||
const computePanels = memoizeOne(
|
||||
(
|
||||
panels: HomeAssistant["panels"],
|
||||
defaultPanel: HomeAssistant["defaultPanel"],
|
||||
panelsOrder: string[],
|
||||
hiddenPanels: string[]
|
||||
): [PanelInfo[], PanelInfo[]] => {
|
||||
if (!panels) {
|
||||
return [[], []];
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
});
|
||||
|
||||
beforeSpacer.sort(panelSorter);
|
||||
afterSpacer.sort(panelSorter);
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
return [beforeSpacer, afterSpacer];
|
||||
};
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(!panel.title && panel.url_path !== defaultPanel)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
});
|
||||
|
||||
const reverseSort = [...panelsOrder].reverse();
|
||||
|
||||
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
||||
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
||||
|
||||
return [beforeSpacer, afterSpacer];
|
||||
}
|
||||
);
|
||||
|
||||
let Sortable;
|
||||
|
||||
let sortStyles: TemplateResult;
|
||||
|
||||
@customElement("ha-sidebar")
|
||||
class HaSidebar extends LitElement {
|
||||
@ -124,16 +175,30 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@internalProperty() private _notifications?: PersistentNotification[];
|
||||
|
||||
@internalProperty() private _editMode = false;
|
||||
|
||||
// property used only in css
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@internalProperty() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
|
||||
private _tooltipHideTimeout?: number;
|
||||
|
||||
private _recentKeydownActiveUntil = 0;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarPanelOrder")
|
||||
private _panelOrder: string[] = [];
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarHiddenPanels")
|
||||
private _hiddenPanels: string[] = [];
|
||||
|
||||
private _sortable?;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
|
||||
@ -141,7 +206,12 @@ class HaSidebar extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(hass);
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
hass.panels,
|
||||
hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
@ -152,9 +222,8 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanel(hass);
|
||||
|
||||
return html`
|
||||
${this._editMode ? sortStyles : ""}
|
||||
<div class="menu">
|
||||
${!this.narrow
|
||||
? html`
|
||||
@ -170,7 +239,13 @@ class HaSidebar extends LitElement {
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<span class="title">Home Assistant</span>
|
||||
<div class="title">
|
||||
${this._editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
DONE
|
||||
</mwc-button>`
|
||||
: "Home Assistant"}
|
||||
</div>
|
||||
</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
@ -179,31 +254,53 @@ class HaSidebar extends LitElement {
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this._editMode,
|
||||
disabled: this._editMode,
|
||||
})}
|
||||
>
|
||||
${this._renderPanel(
|
||||
defaultPanel.url_path,
|
||||
defaultPanel.title || hass.localize("panel.states"),
|
||||
defaultPanel.icon,
|
||||
!defaultPanel.icon ? mdiViewDashboard : undefined
|
||||
)}
|
||||
${beforeSpacer.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
undefined
|
||||
)
|
||||
)}
|
||||
${this._editMode
|
||||
? html`<div id="sortable">
|
||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||
this._renderEmptySortable
|
||||
? ""
|
||||
: this._renderPanels(beforeSpacer)
|
||||
)}
|
||||
</div>`
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
<div class="spacer" disabled></div>
|
||||
|
||||
${afterSpacer.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
undefined
|
||||
)
|
||||
)}
|
||||
${this._editMode && this._hiddenPanels.length
|
||||
? html`
|
||||
${this._hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
>
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.url_path === "lovelace"
|
||||
? "mdi:view-dashboard"
|
||||
: panel.icon}
|
||||
></ha-icon>
|
||||
<span class="item-text"
|
||||
>${panel.url_path === "lovelace"
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="hide-panel"
|
||||
.panel=${url}
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
<div class="spacer" disabled></div>
|
||||
`
|
||||
: ""}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
@ -295,7 +392,9 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_notifications")
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("_editMode") ||
|
||||
changedProps.has("_renderEmptySortable")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@ -361,6 +460,74 @@ class HaSidebar extends LitElement {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
|
||||
if (ev.detail.action !== "hold") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Sortable) {
|
||||
const [sortableImport, sortStylesImport] = await Promise.all([
|
||||
import("sortablejs/modular/sortable.core.esm"),
|
||||
import("./ha-sidebar-sort-styles"),
|
||||
]);
|
||||
|
||||
sortStyles = sortStylesImport.sortStyles;
|
||||
|
||||
Sortable = sortableImport.Sortable;
|
||||
Sortable.mount(sortableImport.OnSpill);
|
||||
Sortable.mount(sortableImport.AutoScroll());
|
||||
}
|
||||
this._editMode = true;
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
this._createSortable();
|
||||
}
|
||||
|
||||
private _createSortable() {
|
||||
this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), {
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
dataIdAttr: "data-panel",
|
||||
onSort: async () => {
|
||||
this._panelOrder = this._sortable.toArray();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _closeEditMode() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
this._editMode = false;
|
||||
}
|
||||
|
||||
private async _hidePanel(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const panel = (ev.target as any).panel;
|
||||
if (this._hiddenPanels.includes(panel)) {
|
||||
return;
|
||||
}
|
||||
// Make a copy for Memoize
|
||||
this._hiddenPanels = [...this._hiddenPanels, panel];
|
||||
this._renderEmptySortable = true;
|
||||
await this.updateComplete;
|
||||
this._renderEmptySortable = false;
|
||||
}
|
||||
|
||||
private async _unhidePanel(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const index = this._hiddenPanels.indexOf((ev.target as any).panel);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
this._hiddenPanels.splice(index, 1);
|
||||
// Make a copy for Memoize
|
||||
this._hiddenPanels = [...this._hiddenPanels];
|
||||
this._renderEmptySortable = true;
|
||||
await this.updateComplete;
|
||||
this._renderEmptySortable = false;
|
||||
}
|
||||
|
||||
private _itemMouseEnter(ev: MouseEvent) {
|
||||
// On keypresses on the listbox, we're going to ignore mouse enter events
|
||||
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
||||
@ -457,6 +624,19 @@ class HaSidebar extends LitElement {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
||||
private _renderPanels(panels: PanelInfo[]) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === "lovelace"
|
||||
? this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.url_path === "lovelace" ? undefined : panel.icon,
|
||||
panel.url_path === "lovelace" ? mdiViewDashboard : undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
@ -480,6 +660,14 @@ class HaSidebar extends LitElement {
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
${this._editMode
|
||||
? html`<ha-svg-icon
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
.path=${mdiClose}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`;
|
||||
@ -542,11 +730,15 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-listbox::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
|
@ -318,10 +318,11 @@ export interface WindowWithLovelaceProm extends Window {
|
||||
export interface ActionHandlerOptions {
|
||||
hasHold?: boolean;
|
||||
hasDoubleClick?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionHandlerDetail {
|
||||
action: string;
|
||||
action: "hold" | "tap" | "double_tap";
|
||||
}
|
||||
|
||||
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;
|
||||
|
@ -2,6 +2,7 @@ import "@material/mwc-ripple";
|
||||
import type { Ripple } from "@material/mwc-ripple";
|
||||
import { directive, PropertyPart } from "lit-html";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||
import {
|
||||
ActionHandlerDetail,
|
||||
ActionHandlerOptions,
|
||||
@ -17,10 +18,18 @@ interface ActionHandler extends HTMLElement {
|
||||
bind(element: Element, options): void;
|
||||
}
|
||||
interface ActionHandlerElement extends HTMLElement {
|
||||
actionHandler?: boolean;
|
||||
actionHandler?: {
|
||||
options: ActionHandlerOptions;
|
||||
start?: (ev: Event) => void;
|
||||
end?: (ev: Event) => void;
|
||||
handleEnter?: (ev: KeyboardEvent) => void;
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"action-handler": ActionHandler;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
action: ActionHandlerDetail;
|
||||
}
|
||||
@ -76,26 +85,45 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
});
|
||||
}
|
||||
|
||||
public bind(element: ActionHandlerElement, options) {
|
||||
if (element.actionHandler) {
|
||||
public bind(element: ActionHandlerElement, options: ActionHandlerOptions) {
|
||||
if (
|
||||
element.actionHandler &&
|
||||
deepEqual(options, element.actionHandler.options)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
element.actionHandler = true;
|
||||
|
||||
element.addEventListener("contextmenu", (ev: Event) => {
|
||||
const e = ev || window.event;
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
e.cancelBubble = true;
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
});
|
||||
if (element.actionHandler) {
|
||||
element.removeEventListener("touchstart", element.actionHandler.start!);
|
||||
element.removeEventListener("touchend", element.actionHandler.end!);
|
||||
element.removeEventListener("touchcancel", element.actionHandler.end!);
|
||||
|
||||
const start = (ev: Event) => {
|
||||
element.removeEventListener("mousedown", element.actionHandler.start!);
|
||||
element.removeEventListener("click", element.actionHandler.end!);
|
||||
|
||||
element.removeEventListener("keyup", element.actionHandler.handleEnter!);
|
||||
} else {
|
||||
element.addEventListener("contextmenu", (ev: Event) => {
|
||||
const e = ev || window.event;
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
e.cancelBubble = true;
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
element.actionHandler = { options };
|
||||
|
||||
if (options.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.actionHandler.start = (ev: Event) => {
|
||||
this.held = false;
|
||||
let x;
|
||||
let y;
|
||||
@ -107,13 +135,19 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
y = (ev as MouseEvent).pageY;
|
||||
}
|
||||
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.startAnimation(x, y);
|
||||
this.held = true;
|
||||
}, this.holdTime);
|
||||
if (options.hasHold) {
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.startAnimation(x, y);
|
||||
this.held = true;
|
||||
}, this.holdTime);
|
||||
}
|
||||
};
|
||||
|
||||
const end = (ev: Event) => {
|
||||
element.actionHandler.end = (ev: Event) => {
|
||||
// Don't respond on our own generated click
|
||||
if (!ev.isTrusted) {
|
||||
return;
|
||||
}
|
||||
// Prevent mouse event if touch event
|
||||
ev.preventDefault();
|
||||
if (
|
||||
@ -122,9 +156,11 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
if (options.hasHold) {
|
||||
clearTimeout(this.timer);
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
}
|
||||
if (this.held) {
|
||||
fireEvent(element, "action", { action: "hold" });
|
||||
} else if (options.hasDoubleClick) {
|
||||
@ -143,24 +179,30 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
}
|
||||
} else {
|
||||
fireEvent(element, "action", { action: "tap" });
|
||||
// Fire the click we prevented the action for
|
||||
(ev.target as HTMLElement)?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (ev: KeyboardEvent) => {
|
||||
element.actionHandler.handleEnter = (ev: KeyboardEvent) => {
|
||||
if (ev.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
end(ev);
|
||||
(ev.currentTarget as ActionHandlerElement).actionHandler!.end!(ev);
|
||||
};
|
||||
|
||||
element.addEventListener("touchstart", start, { passive: true });
|
||||
element.addEventListener("touchend", end);
|
||||
element.addEventListener("touchcancel", end);
|
||||
element.addEventListener("touchstart", element.actionHandler.start, {
|
||||
passive: true,
|
||||
});
|
||||
element.addEventListener("touchend", element.actionHandler.end);
|
||||
element.addEventListener("touchcancel", element.actionHandler.end);
|
||||
|
||||
element.addEventListener("mousedown", start, { passive: true });
|
||||
element.addEventListener("click", end);
|
||||
element.addEventListener("mousedown", element.actionHandler.start, {
|
||||
passive: true,
|
||||
});
|
||||
element.addEventListener("click", element.actionHandler.end);
|
||||
|
||||
element.addEventListener("keyup", handleEnter);
|
||||
element.addEventListener("keyup", element.actionHandler.handleEnter);
|
||||
}
|
||||
|
||||
private startAnimation(x: number, y: number) {
|
||||
|
@ -10975,6 +10975,11 @@ sockjs@0.3.19:
|
||||
faye-websocket "^0.10.0"
|
||||
uuid "^3.0.1"
|
||||
|
||||
sortablejs@^1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
|
||||
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
|
Loading…
x
Reference in New Issue
Block a user