Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/jsxc/jsxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsualko <klaus@jsxc.org>2022-01-02 19:17:16 +0300
committersualko <klaus@jsxc.org>2022-01-02 19:17:16 +0300
commitbe92eb22be7865ef61894f41a73409a7a1399fd3 (patch)
tree0bdb361d096616a40a5d7e1acfc5e1861a82edaa
parent789d4b07052128b92d13a1bf04e52db3b7f29ea0 (diff)
feat: add chat message context menu
-rw-r--r--fonts/jsxc-icons.eotbin11572 -> 11752 bytes
-rw-r--r--fonts/jsxc-icons.scss18
-rw-r--r--fonts/jsxc-icons.woffbin6272 -> 6372 bytes
-rw-r--r--fonts/jsxc-icons.woff2bin5192 -> 5260 bytes
-rw-r--r--images/icons/quotation.svg60
-rw-r--r--scss/partials/_menu.scss27
-rw-r--r--scss/partials/_window.scss153
-rw-r--r--src/Account.ts11
-rw-r--r--src/Menu.ts19
-rw-r--r--src/MenuChatMessage.ts22
-rw-r--r--src/MenuItemFactory.interface.ts11
-rw-r--r--src/MenuItemStaticFactory.ts19
-rw-r--r--src/ui/ChatWindow.ts2
-rw-r--r--src/ui/ChatWindowMessage.ts20
-rw-r--r--src/ui/MenuComponent.ts127
-rw-r--r--src/ui/util/LongPress.ts32
-rw-r--r--template/menu.hbs7
17 files changed, 459 insertions, 69 deletions
diff --git a/fonts/jsxc-icons.eot b/fonts/jsxc-icons.eot
index f8314df0..429e5b2d 100644
--- a/fonts/jsxc-icons.eot
+++ b/fonts/jsxc-icons.eot
Binary files differ
diff --git a/fonts/jsxc-icons.scss b/fonts/jsxc-icons.scss
index c3be0b55..89d4e8b3 100644
--- a/fonts/jsxc-icons.scss
+++ b/fonts/jsxc-icons.scss
@@ -2,9 +2,9 @@ $jsxc-icons-font: "jsxc-icons";
@font-face {
font-family: $jsxc-icons-font;
- src: url("../fonts/jsxc-icons.eot?1a3d16709418122adaef6fedd3d0c8fe#iefix") format("embedded-opentype"),
-url("../fonts/jsxc-icons.woff2?1a3d16709418122adaef6fedd3d0c8fe") format("woff2"),
-url("../fonts/jsxc-icons.woff?1a3d16709418122adaef6fedd3d0c8fe") format("woff");
+ src: url("../fonts/jsxc-icons.eot?d795960dd0062a30f89bb22c3b0df55e#iefix") format("embedded-opentype"),
+url("../fonts/jsxc-icons.woff2?d795960dd0062a30f89bb22c3b0df55e") format("woff2"),
+url("../fonts/jsxc-icons.woff?d795960dd0062a30f89bb22c3b0df55e") format("woff");
}
[class^="jsxc-icon-"]:before, [class*=" jsxc-icon-"]:before {
@@ -56,10 +56,11 @@ $jsxc-icons-map: (
"pick-up-disabled": "\f123",
"pick-up": "\f124",
"placeholder": "\f125",
- "resize": "\f126",
- "search": "\f127",
- "smiley": "\f128",
- "speech-balloon": "\f129",
+ "quotation": "\f126",
+ "resize": "\f127",
+ "search": "\f128",
+ "smiley": "\f129",
+ "speech-balloon": "\f12a",
);
.jsxc-icon-attachment:before {
@@ -173,6 +174,9 @@ $jsxc-icons-map: (
.jsxc-icon-placeholder:before {
content: map-get($jsxc-icons-map, "placeholder");
}
+.jsxc-icon-quotation:before {
+ content: map-get($jsxc-icons-map, "quotation");
+}
.jsxc-icon-resize:before {
content: map-get($jsxc-icons-map, "resize");
}
diff --git a/fonts/jsxc-icons.woff b/fonts/jsxc-icons.woff
index 3c29036b..3c9379d6 100644
--- a/fonts/jsxc-icons.woff
+++ b/fonts/jsxc-icons.woff
Binary files differ
diff --git a/fonts/jsxc-icons.woff2 b/fonts/jsxc-icons.woff2
index df7ebe09..4784938e 100644
--- a/fonts/jsxc-icons.woff2
+++ b/fonts/jsxc-icons.woff2
Binary files differ
diff --git a/images/icons/quotation.svg b/images/icons/quotation.svg
new file mode 100644
index 00000000..7a7b98c5
--- /dev/null
+++ b/images/icons/quotation.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="44"
+ height="44"
+ version="1.1"
+ id="svg4"
+ sodipodi:docname="quotation.svg"
+ inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1999"
+ inkscape:window-height="1265"
+ id="namedview6"
+ showgrid="false"
+ inkscape:zoom="5.3636364"
+ inkscape:cx="-5.5"
+ inkscape:cy="22"
+ inkscape:window-x="1562"
+ inkscape:window-y="280"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4" />
+ <g
+ aria-label="&quot;"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.33333397px;line-height:125%;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text819"
+ transform="matrix(7.8234615,0,0,7.8234615,-143.85981,-87.522429)">
+ <path
+ d="m 19.579639,16.321897 0.277334,-0.208 0.849333,-2.426667 0.052,-0.624 -0.06933,-0.485333 -0.208,-0.329333 -0.502667,-0.398667 -0.398667,-0.138667 h -0.294666 l -0.416,0.138667 -0.06933,0.173333 -0.138666,0.08667 -0.104,0.225333 -0.138667,0.814667 0.173333,0.208 0.364,0.208 0.416,0.416 0.173334,0.988 z m 3.224,-0.03467 0.277334,-0.208 0.849333,-2.426666 0.052,-0.624 -0.06933,-0.485334 -0.208,-0.329333 -0.502667,-0.398667 -0.398667,-0.138666 h -0.294666 l -0.416,0.138666 -0.06933,0.173334 -0.138666,0.08667 -0.104,0.225334 -0.138667,0.814666 0.173333,0.208 0.364,0.208 0.416,0.416 0.173334,0.988 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Bocadillo;-inkscape-font-specification:Bocadillo"
+ id="path821"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/scss/partials/_menu.scss b/scss/partials/_menu.scss
index 51e38173..1b98fc26 100644
--- a/scss/partials/_menu.scss
+++ b/scss/partials/_menu.scss
@@ -4,11 +4,13 @@
&__button {
align-items: flex-start;
+ color: "inherit";
cursor: pointer;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
+ text-decoration: none;
user-select: none;
white-space: nowrap;
@@ -65,7 +67,8 @@
&--drop-bottom-left,
&--drop-bottom-right,
&--drop-right-top,
- &--vertical-left {
+ &--vertical-left
+ &--vertical-right {
.jsxc-menu__content {
border-radius: 3px;
max-height: 9999px;
@@ -133,6 +136,28 @@
}
}
+ &--vertical-right {
+ .jsxc-menu__content {
+ left: 100%;
+ top: 0;
+
+ &::after {
+ right: 100%;
+ top: 14px;
+ transform: rotate(90deg);
+ }
+ }
+
+ ul {
+ display: flex;
+ padding: 0;
+ }
+
+ li {
+ height: 42px;
+ }
+ }
+
&--drop-bottom-left {
.jsxc-menu__content {
bottom: 100%;
diff --git a/scss/partials/_window.scss b/scss/partials/_window.scss
index 82ff23e4..334f76ce 100644
--- a/scss/partials/_window.scss
+++ b/scss/partials/_window.scss
@@ -346,82 +346,113 @@
}
}
- p {
- clear: both;
- font-size: 1em;
- margin: 0;
-
- +p {
- margin-top: 0.7em;
+ .jsxc-content {
+ p {
+ clear: both;
+ font-size: 1em;
+ margin: 0;
+
+ +p {
+ margin-top: 0.7em;
+ }
}
- }
-
- a {
- color: $chatmessage-a;
- display: inline-block;
- max-width: 100%;
- position: relative;
- text-decoration: underline;
- &[download]::before {
- background-color: rgba(255, 255, 255, 0.7);
- background-image: url("../images/download_icon_black.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: 3em;
- border-radius: 3px;
- bottom: 5px;
- content: " ";
- left: 0;
- opacity: 0;
- position: absolute;
- right: 0;
- top: 0;
- transition: opacity 0.5s;
- }
+ a {
+ color: $chatmessage-a;
+ display: inline-block;
+ max-width: 100%;
+ position: relative;
+ text-decoration: underline;
- &[download]:hover {
- &::before {
- opacity: 0.6;
+ &[download]::before {
+ background-color: rgba(255, 255, 255, 0.7);
+ background-image: url("../images/download_icon_black.svg");
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: 3em;
+ border-radius: 3px;
+ bottom: 5px;
+ content: " ";
+ left: 0;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: opacity 0.5s;
}
- }
- &.jsxc-geo {
- background-color: #fff;
- border-radius: 0.8em;
- display: block;
- padding: 1em 1em 1em 3em;
+ &[download]:hover {
+ &::before {
+ opacity: 0.6;
+ }
+ }
- &::before {
+ &.jsxc-geo {
background-color: #fff;
- background-image: url("../images/location_icon.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: contain;
- content: "";
- display: inline-block;
- height: 2em;
- margin-left: -2em;
- vertical-align: bottom;
- width: 2em;
+ border-radius: 0.8em;
+ display: block;
+ padding: 1em 1em 1em 3em;
+
+ &::before {
+ background-color: #fff;
+ background-image: url("../images/location_icon.svg");
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ content: "";
+ display: inline-block;
+ height: 2em;
+ margin-left: -2em;
+ vertical-align: bottom;
+ width: 2em;
+ }
}
}
+
+ img {
+ max-width: 100%;
+ }
+
+ .jsxc-avatar {
+ display: none;
+ }
+
+ .jsxc-quote {
+ border-left: 3px solid #999;
+ display: inline-block;
+ opacity: 0.7;
+ padding: 0 3px 0 5px;
+ width: 100%; // margin-bottom: 5px;
+ }
}
- img {
- max-width: 100%;
+ &:hover .jsxc-menu__button {
+ visibility: visible !important;
}
- .jsxc-avatar {
+ .jsxc-menu {
display: none;
- }
+ position: absolute;
+ top: 0;
- .jsxc-quote {
- border-left: 3px solid #999;
- display: inline-block;
- opacity: 0.7;
- padding: 0 3px 0 5px;
- width: 100%; // margin-bottom: 5px;
+ .jsxc-menu__button {
+ height: 42px;
+ width: 42px;
+ }
+
+ &:not(.jsxc-menu--opened) .jsxc-menu__button {
+ visibility: hidden;
+ }
+
+ &.jsxc-menu--vertical-right {
+ display: block;
+ right: 100%;
+ }
+
+ &.jsxc-menu--vertical-left {
+ display: block;
+ left: 100%;
+ }
}
}
diff --git a/src/Account.ts b/src/Account.ts
index 6a007ec9..ba9aa61e 100644
--- a/src/Account.ts
+++ b/src/Account.ts
@@ -25,6 +25,7 @@ import CommandRepository from './CommandRepository';
import AvatarSet from '@ui/AvatarSet';
import { IAvatar } from './Avatar.interface';
import CallManager from './CallManager';
+import MenuChatMessage from './MenuChatMessage';
type ConnectionCallback = (status: number, condition?: string) => void;
@@ -59,6 +60,8 @@ export default class Account {
private callManager: CallManager;
+ private chatMessageMenu: MenuChatMessage;
+
private options: Options;
private pipes = {};
@@ -262,6 +265,14 @@ export default class Account {
return this.callManager;
}
+ public getChatMessageMenu(): MenuChatMessage {
+ if (!this.chatMessageMenu) {
+ this.chatMessageMenu = new MenuChatMessage();
+ }
+
+ return this.chatMessageMenu;
+ }
+
public getContact(jid?: IJID): IContact {
return jid && jid.bare !== this.getJID().bare ? this.getContactManager().getContact(jid) : this.contact;
}
diff --git a/src/Menu.ts b/src/Menu.ts
new file mode 100644
index 00000000..a2c50be7
--- /dev/null
+++ b/src/Menu.ts
@@ -0,0 +1,19 @@
+import IMenuItemFactory, { MenuItem } from './MenuItemFactory.interface';
+
+export default class Menu<Params extends any[] = any[]> {
+ constructor(private menuItems: IMenuItemFactory<Params>[] = []) {}
+
+ public getMenuItems(...args: Params): MenuItem[] {
+ return this.menuItems
+ .map(menuItem => menuItem.generate(...args))
+ .filter(menuItem => menuItem !== false) as MenuItem[];
+ }
+
+ public registerMenuItem(menuItem: IMenuItemFactory<Params>) {
+ this.menuItems.push(menuItem);
+ }
+
+ public removeMenuItem(menuItem: IMenuItemFactory<Params>) {
+ this.menuItems = this.menuItems.filter(item => item !== menuItem);
+ }
+}
diff --git a/src/MenuChatMessage.ts b/src/MenuChatMessage.ts
new file mode 100644
index 00000000..1f72ea4a
--- /dev/null
+++ b/src/MenuChatMessage.ts
@@ -0,0 +1,22 @@
+import { IContact } from './Contact.interface';
+import Menu from './Menu';
+import MenuItemStaticFactory from './MenuItemStaticFactory';
+import { IMessage } from './Message.interface';
+
+function quoteMessage(contact: IContact, message: IMessage) {
+ const chatWindow = contact.getChatWindow();
+ const inputText = chatWindow.getInput();
+ const quote = message
+ .getPlaintextMessage()
+ .split('\n')
+ .map(line => '> ' + line)
+ .join('\n');
+
+ chatWindow.setInput(inputText + (!inputText || inputText.endsWith('\n\n') ? '' : '\n\n') + quote + '\n\n');
+}
+
+export default class MenuChatMessage extends Menu<[IContact, IMessage]> {
+ constructor() {
+ super([new MenuItemStaticFactory('core-quote', '', quoteMessage, 'quotation')]);
+ }
+}
diff --git a/src/MenuItemFactory.interface.ts b/src/MenuItemFactory.interface.ts
new file mode 100644
index 00000000..0d8f8b70
--- /dev/null
+++ b/src/MenuItemFactory.interface.ts
@@ -0,0 +1,11 @@
+export type MenuItem = {
+ id: string;
+ label: string;
+ icon?: string;
+ disabled?: boolean;
+ handler: (ev: Event) => void;
+};
+
+export default interface IMenuItemFactory<Params extends any[]> {
+ generate: (...args: Params) => MenuItem | false;
+}
diff --git a/src/MenuItemStaticFactory.ts b/src/MenuItemStaticFactory.ts
new file mode 100644
index 00000000..cbd44959
--- /dev/null
+++ b/src/MenuItemStaticFactory.ts
@@ -0,0 +1,19 @@
+import IMenuItemFactory, { MenuItem } from './MenuItemFactory.interface';
+
+export default class MenuItemStaticFactory<Params extends any[] = any[]> implements IMenuItemFactory<Params> {
+ constructor(
+ private id: string,
+ private label: string,
+ private handler: (...args: Params) => void,
+ private icon?: string
+ ) {}
+
+ public generate(...args: Params): MenuItem {
+ return {
+ id: this.id,
+ label: this.label,
+ handler: () => this.handler(...args),
+ icon: this.icon,
+ };
+ }
+}
diff --git a/src/ui/ChatWindow.ts b/src/ui/ChatWindow.ts
index ba25c670..a3d6e429 100644
--- a/src/ui/ChatWindow.ts
+++ b/src/ui/ChatWindow.ts
@@ -190,6 +190,8 @@ export default class ChatWindow {
public setInput(text: string) {
this.inputElement.val(text);
this.inputElement.trigger('focus');
+
+ this.resizeInputArea();
}
public appendTextToInput(text: string = '') {
diff --git a/src/ui/ChatWindowMessage.ts b/src/ui/ChatWindowMessage.ts
index 5bafa4ab..60538dd5 100644
--- a/src/ui/ChatWindowMessage.ts
+++ b/src/ui/ChatWindowMessage.ts
@@ -7,6 +7,8 @@ import LinkHandlerGeo from '@src/LinkHandlerGeo';
import Color from '@util/Color';
import Translation from '@util/Translation';
import messageHistory from './dialogs/messageHistory';
+import MenuComponent from './MenuComponent';
+import onLongPress from './util/LongPress';
let chatWindowMessageTemplate = require('../../template/chat-window-message.hbs');
@@ -27,6 +29,8 @@ export default class ChatWindowMessage {
this.generateElement();
this.registerHooks();
+
+ this.initMenu();
}
public getElement() {
@@ -237,6 +241,22 @@ export default class ChatWindowMessage {
}
}
+ private initMenu() {
+ if (this.message.isSystem()) {
+ return;
+ }
+
+ const messageMenu = this.chatWindow.getAccount().getChatMessageMenu();
+ const menuType = this.message.isOutgoing() ? 'vertical-right' : 'vertical-left';
+ const menu = new MenuComponent('more', menuType, messageMenu, [this.chatWindow.getContact(), this.message]);
+
+ this.element.append(menu.getElement());
+
+ onLongPress(this.element, () => {
+ menu.toggle();
+ });
+ }
+
private registerHooks() {
this.message.registerHook('replacedBy', () => {
const chatWindowMessageReplacement = new ChatWindowMessage(this.originalMessage, this.chatWindow);
diff --git a/src/ui/MenuComponent.ts b/src/ui/MenuComponent.ts
new file mode 100644
index 00000000..c3112ed0
--- /dev/null
+++ b/src/ui/MenuComponent.ts
@@ -0,0 +1,127 @@
+import Menu from '@src/Menu';
+
+const CLASSNAME_OPENED = 'jsxc-menu--opened';
+const menuTemplate = require('../../template/menu.hbs');
+
+export default class MenuComponent<Params extends unknown[]> {
+ private element: JQuery<HTMLElement>;
+
+ private timer: number;
+
+ constructor(
+ label: string | { text: string; icon: string },
+ type: 'pushup' | 'vertical-left' | 'vertical-right',
+ private menu: Menu<Params>,
+ private params: Params,
+ theme: 'dark' | 'light' = 'light'
+ ) {
+ this.element = $(
+ menuTemplate({
+ classes: `jsxc-menu--${type} jsxc-menu--${theme}`,
+ labelText: typeof label === 'object' ? label.text : '',
+ })
+ );
+
+ const icon = typeof label === 'string' ? label : label.icon;
+
+ if (icon) {
+ this.element.find('.jsxc-menu__button').prepend($('<i>').addClass(`jsxc-icon-${icon} jsxc-icon--center`));
+ }
+
+ this.registerHandlers();
+ }
+
+ public getElement(): JQuery<HTMLElement> {
+ return this.element;
+ }
+
+ public getButtonElement(): JQuery {
+ return this.element.find('.jsxc-menu__button');
+ }
+
+ public toggle(): void {
+ if (this.element.hasClass(CLASSNAME_OPENED)) {
+ this.closeMenu();
+ } else {
+ this.openMenu();
+ }
+ }
+
+ private addEntry(
+ label: string,
+ handler: (ev: JQuery.ClickEvent) => void,
+ icon?: string,
+ disabled?: boolean
+ ): JQuery {
+ let itemElement = $('<li>');
+
+ if (disabled) {
+ itemElement.addClass('jsxc-disabled');
+ }
+
+ itemElement.text(label);
+ itemElement.on('click', handler);
+
+ if (icon) {
+ itemElement.prepend($('<i>').addClass(`jsxc-icon-${icon} jsxc-icon--center`));
+ }
+
+ this.element.find('ul').append(itemElement);
+
+ return itemElement;
+ }
+
+ private clearEntries() {
+ this.element.find('ul').empty();
+ }
+
+ private registerHandlers() {
+ this.element.on('click', this.onClick);
+ this.element.on('mouseleave', this.onMouseLeave);
+ this.element.on('mouseenter', this.onMouseEnter);
+ }
+
+ private onClick = (ev: JQuery.ClickEvent) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ this.toggle();
+ };
+
+ private onMouseLeave = () => {
+ if (this.element.hasClass(CLASSNAME_OPENED)) {
+ this.timer = window.setTimeout(this.closeMenu, 2000);
+ }
+ };
+
+ private onMouseEnter = () => {
+ window.clearTimeout(this.timer);
+ };
+
+ private openMenu = () => {
+ this.clearEntries();
+
+ this.menu.getMenuItems(...this.params).map(({ label, handler, disabled, icon }) => {
+ this.addEntry(label, (ev: JQuery.ClickEvent) => handler(ev.originalEvent), icon, disabled);
+ });
+
+ $('body').off('click', null, this.closeMenu);
+
+ // hide other lists
+ $('body').trigger('click');
+
+ window.clearTimeout(this.timer);
+
+ this.element.addClass(CLASSNAME_OPENED);
+
+ $('body').on('click', this.closeMenu);
+ };
+
+ private closeMenu = () => {
+ this.element.removeClass(CLASSNAME_OPENED);
+
+ this.clearEntries();
+
+ $('body').off('click', null, this.closeMenu);
+ };
+}
diff --git a/src/ui/util/LongPress.ts b/src/ui/util/LongPress.ts
new file mode 100644
index 00000000..166aa18d
--- /dev/null
+++ b/src/ui/util/LongPress.ts
@@ -0,0 +1,32 @@
+const onLongPress = (element: JQuery<HTMLElement>, handler: () => void): void => {
+ const duration = 1000;
+
+ let durationTimeout: number;
+ let isLongPress: boolean;
+
+ element.on('mousedown', ev => {
+ isLongPress = false;
+
+ durationTimeout = window.setTimeout(() => {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ isLongPress = true;
+
+ handler();
+ }, duration);
+ });
+
+ element.on('mouseup', () => {
+ window.clearTimeout(durationTimeout);
+ });
+
+ element.on('click', ev => {
+ if (isLongPress) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ });
+};
+
+export default onLongPress;
diff --git a/template/menu.hbs b/template/menu.hbs
new file mode 100644
index 00000000..b1335628
--- /dev/null
+++ b/template/menu.hbs
@@ -0,0 +1,7 @@
+<div class="jsxc-menu {{classes}}">
+ <a class="jsxc-menu__button jsxc-icon--clickable">{{label-text}}</a>
+ <div class="jsxc-menu__content">
+ <ul>
+ </ul>
+ </div>
+</div>