wx-svelte-menu 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/license.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 XB Software Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "wx-svelte-menu",
3
+ "version": "1.3.0",
4
+ "productTag": "menu",
5
+ "productTrial": false,
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "build:dist": "vite build --mode dist",
10
+ "build:tests": "vite build --mode test",
11
+ "lint": "yarn eslint ./demos ./src --ext .svelte,.ts,.js",
12
+ "start": "vite --open=/demos/",
13
+ "start:tests": "vite --open=/tests/ --host 0.0.0.0 --port 5100 --mode test",
14
+ "test": "true",
15
+ "test:cypress": "cypress run -P ./ --config \"baseUrl=http://localhost:5100/tests\""
16
+ },
17
+ "svelte": "src/index.js",
18
+ "exports": {
19
+ ".": {
20
+ "svelte": "./src/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/svar-widgets/menu.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://forum.svar.dev"
31
+ },
32
+ "homepage": "https://svar.dev/svelte/core/",
33
+ "dependencies": {
34
+ "wx-svelte-core": "1.3.0",
35
+ "wx-lib-dom": "0.6.0"
36
+ },
37
+ "files": [
38
+ "src",
39
+ "readme.md",
40
+ "whatsnew.md",
41
+ "license.txt"
42
+ ]
43
+ }
package/readme.md ADDED
@@ -0,0 +1,60 @@
1
+ ### SVAR Menu for Svelte
2
+
3
+ SVAR Menu provides ready to use control for creating context and popup menus
4
+
5
+ ### Useful Links
6
+
7
+ - [Documentation](https://docs.svar.dev/svelte/core/overview)
8
+ - [How to start guide](https://docs.svar.dev/svelte/core/getting_started/)
9
+ - [Demos](https://docs.svar.dev/svelte/menu/samples/#/base/willow)
10
+
11
+ ### License
12
+
13
+ SVAR Menu for Svelte is available under MIT license.
14
+
15
+ ### How to Use
16
+
17
+ To use the widget, simply import the package and include the component in your Svelte file:
18
+
19
+ ```svelte
20
+ <script>
21
+ import { Menu } from "wx-svelte-menu";
22
+
23
+ function onClick(item) {
24
+ const action = ev.detail.action;
25
+ message = action ? `clicked on ${action.id}` : "closed";
26
+ }
27
+
28
+ const options = [
29
+ { id: "edit-cut", text: "Cut", icon: "wxi wxi-content-cut" },
30
+ { id: "edit-copy", text: "Copy", icon: "wxi wxi-content-copy" },
31
+ {
32
+ id: "edit-paste",
33
+ text: "Paste",
34
+ icon: "wxi wxi-content-paste",
35
+ },
36
+ ];
37
+ </script>
38
+
39
+ <Menu {options} on:click={clicked} />
40
+ ```
41
+
42
+ ### How to Modify
43
+
44
+ Typically, you don't need to modify the code. However, if you wish to do so, follow these steps:
45
+
46
+ 1. Run `yarn` to install dependencies. Note that this project is a monorepo using `yarn` workspaces, so npm will not work
47
+ 2. Start the project in development mode with `yarn start`
48
+
49
+ ### Run Tests
50
+
51
+ To run the test:
52
+
53
+ 1. Start the test examples with:
54
+ ```sh
55
+ yarn start:tests
56
+ ```
57
+ 2. In a separate console, run the end-to-end tests with:
58
+ ```sh
59
+ yarn test:cypress
60
+ ```
@@ -0,0 +1,97 @@
1
+ <script>
2
+ import { Portal } from "wx-svelte-core";
3
+ import { id } from "wx-lib-dom";
4
+ import Menu from "./Menu.svelte";
5
+ import { filterMenu } from "../helpers";
6
+
7
+ import { createEventDispatcher } from "svelte";
8
+ const dispatch = createEventDispatcher();
9
+
10
+ const SLOTS = $$props.$$slots;
11
+
12
+ export let options;
13
+ export let at = "bottom";
14
+ export let resolver = null;
15
+ export let dataKey = "contextId";
16
+ export let filter = null;
17
+ export let css = "";
18
+ export const handler = show;
19
+
20
+ var filteredOptions;
21
+ $: filteredOptions = options;
22
+
23
+ var item = null;
24
+ var parent = null;
25
+ let left = 0,
26
+ top = 0;
27
+
28
+ function onClick(ev) {
29
+ parent = null;
30
+ dispatch("click", ev.detail);
31
+ }
32
+ function getDataAttr(node, name) {
33
+ let v = null;
34
+ while (node && node.dataset && !v) {
35
+ v = node.dataset[name];
36
+ node = node.parentNode;
37
+ }
38
+ return v ? id(v) : null;
39
+ }
40
+ function show(ev, obj) {
41
+ if (!ev) {
42
+ parent = null;
43
+ return;
44
+ }
45
+
46
+ if (ev.defaultPrevented) return;
47
+
48
+ const target = ev.target;
49
+ if (target && target.dataset && target.dataset.menuIgnore) return;
50
+
51
+ left = ev.clientX + 1;
52
+ top = ev.clientY + 1;
53
+
54
+ item = typeof obj !== "undefined" ? obj : getDataAttr(target, dataKey);
55
+ if (resolver) {
56
+ item = resolver(item, ev);
57
+ if (!item) return;
58
+ }
59
+
60
+ if (item !== null && filter) {
61
+ filteredOptions = filterMenu(options, v => filter(v, item));
62
+ }
63
+ parent = target;
64
+
65
+ ev.preventDefault();
66
+ }
67
+
68
+ // about #key in markup below
69
+ // we need to be sure that menu is closed before it is shown again
70
+ // its the only way to reinit click-outside
71
+ // otherwise, the menu will be hidden as click-ouside occurs after show call
72
+ </script>
73
+
74
+ {#if SLOTS && SLOTS.default}
75
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
76
+ <div on:click={handler} data-menu-ignore="true">
77
+ <slot />
78
+ </div>
79
+ {/if}
80
+
81
+ {#if parent}
82
+ <Portal let:mount>
83
+ {#key parent}
84
+ <Menu
85
+ {css}
86
+ {at}
87
+ {top}
88
+ {left}
89
+ {mount}
90
+ {parent}
91
+ context={item}
92
+ on:click={onClick}
93
+ options={filteredOptions}
94
+ />
95
+ {/key}
96
+ </Portal>
97
+ {/if}
@@ -0,0 +1,31 @@
1
+ <script>
2
+ import ActionMenu from "./ActionMenu.svelte";
3
+
4
+ const SLOTS = $$props.$$slots;
5
+
6
+ export let handler = null;
7
+
8
+ export let options;
9
+ export let at = "bottom";
10
+ export let resolver = null;
11
+ export let dataKey = "contextId";
12
+ export let filter = null;
13
+ export let css = "";
14
+ </script>
15
+
16
+ {#if SLOTS && SLOTS.default}
17
+ <div on:contextmenu={handler} data-menu-ignore="true">
18
+ <slot />
19
+ </div>
20
+ {/if}
21
+
22
+ <ActionMenu
23
+ {css}
24
+ {at}
25
+ {options}
26
+ {resolver}
27
+ {dataKey}
28
+ {filter}
29
+ bind:handler
30
+ on:click
31
+ />
@@ -0,0 +1,40 @@
1
+ <script>
2
+ import { Portal } from "wx-svelte-core";
3
+ import Menu from "./Menu.svelte";
4
+ import { createEventDispatcher } from "svelte";
5
+ const dispatch = createEventDispatcher();
6
+
7
+ export let options;
8
+ export let at = "bottom";
9
+ export let css = "";
10
+
11
+ export const handler = ev => {
12
+ parent = ev.target;
13
+ ev.preventDefault();
14
+ };
15
+
16
+ var parent = null;
17
+ function onClick(ev) {
18
+ parent = null;
19
+ dispatch("click", ev.detail);
20
+ }
21
+ function show(ev) {
22
+ let target = ev.target;
23
+ while (!target.dataset.menuIgnore) {
24
+ parent = target;
25
+ target = target.parentNode;
26
+ }
27
+ }
28
+ </script>
29
+
30
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
31
+ <div on:click={show} data-menu-ignore="true">
32
+ <slot />
33
+ </div>
34
+ {#if parent}
35
+ <Portal let:mount>
36
+ {#key parent}
37
+ <Menu {css} {at} {mount} {parent} {options} on:click={onClick} />
38
+ {/key}
39
+ </Portal>
40
+ {/if}
@@ -0,0 +1,113 @@
1
+ <script>
2
+ import { clickOutside, calculatePosition } from "wx-lib-dom";
3
+ import { onMount, createEventDispatcher } from "svelte";
4
+
5
+ import MenuItem from "./MenuItem.svelte";
6
+ import { prepareMenuData } from "../helpers";
7
+
8
+ const dispatch = createEventDispatcher();
9
+
10
+ export let options;
11
+ $: prepareMenuData(options);
12
+
13
+ export let left = 0;
14
+ export let top = 0;
15
+ export let at = "bottom";
16
+ export let parent = null;
17
+ export let mount = null;
18
+ export let context = null;
19
+ export let css = "";
20
+
21
+ let x = -10000;
22
+ let y = -10000;
23
+ let index = 0;
24
+ let width;
25
+
26
+ let self;
27
+ let showSub;
28
+ let activeItem;
29
+
30
+ function updatePosition() {
31
+ const result = calculatePosition(self, parent, at, left, top);
32
+ x = result.x || x;
33
+ y = result.y || y;
34
+ index = result.index;
35
+ width = result?.width || width;
36
+ }
37
+
38
+ // unfortunately svelte doesn't guarantee that afterUpdate of child component
39
+ // will be called after onMount in the parent one, so to be sure that Portal is already
40
+ // moved menu to the correct parent we are registering single time handler
41
+ if (mount) mount(updatePosition);
42
+ onMount(updatePosition);
43
+ $: updatePosition(parent);
44
+
45
+ function onLeave() {
46
+ showSub = false;
47
+ }
48
+ function cancel() {
49
+ dispatch("click", { action: null });
50
+ }
51
+ </script>
52
+
53
+ <div
54
+ use:clickOutside={{ callback: cancel, modal: true }}
55
+ bind:this={self}
56
+ data-wx-menu="true"
57
+ class="wx-menu {css}"
58
+ style="top:{y}px;left:{x}px;width:{width};"
59
+ on:mouseleave={onLeave}
60
+ >
61
+ {#each options as item (item.id)}
62
+ {#if item.type === "separator"}
63
+ <div class="wx-separator" />
64
+ {:else}
65
+ <MenuItem
66
+ {item}
67
+ bind:showSub
68
+ bind:activeItem
69
+ on:click={ev => {
70
+ if (!item.data && !ev.defaultPrevented) {
71
+ const pack = { context, action: item, event: ev };
72
+ if (item.handler) item.handler(pack);
73
+ dispatch("click", pack);
74
+
75
+ // it is a rare case when we need to stop event bubbling
76
+ // clicking on menu is isolated action which must not affect any other elements on the page
77
+ ev.stopPropagation();
78
+ }
79
+ }}
80
+ />
81
+ {/if}
82
+ {#if item.data && showSub === item.id}
83
+ <svelte:self
84
+ {css}
85
+ options={item.data}
86
+ at="right-overlap"
87
+ parent={activeItem}
88
+ {context}
89
+ on:click
90
+ />
91
+ {/if}
92
+ {/each}
93
+ </div>
94
+
95
+ <style>
96
+ .wx-menu {
97
+ position: absolute;
98
+ box-shadow: var(--wx-shadow-light);
99
+
100
+ min-width: 125px;
101
+ display: flex;
102
+ flex-direction: column;
103
+ z-index: 20;
104
+ border-radius: var(--wx-border-radius);
105
+ background-color: var(--wx-background);
106
+ padding: 4px 0;
107
+ }
108
+
109
+ .wx-separator {
110
+ width: 100%;
111
+ border-top: var(--wx-border-medium);
112
+ }
113
+ </style>
@@ -0,0 +1,99 @@
1
+ <script>
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ import ActionMenu from "./ActionMenu.svelte";
5
+ import { prepareMenuData } from "../helpers";
6
+
7
+ const dispatch = createEventDispatcher();
8
+
9
+ export let css = "";
10
+ export let menuCss = "";
11
+ export let options;
12
+ $: prepareMenuData(options);
13
+
14
+ let active;
15
+ let menuOptions = [];
16
+ let activate = null;
17
+
18
+ function doClick(ev) {
19
+ active = null;
20
+ dispatch("click", ev.detail);
21
+ }
22
+
23
+ function setMenu(ev, item, trigger) {
24
+ // if the item has a submenu, show it and enable hover mode
25
+ if (item.data && item.data.length) {
26
+ if (active && trigger) {
27
+ // second click on item with submenu disables hover mode
28
+ active = null;
29
+ } else {
30
+ menuOptions = item.data;
31
+ active = item.id;
32
+ activate(ev, item);
33
+ }
34
+ } else {
35
+ // hide the submenu
36
+ activate(null);
37
+ // if it was the click action, dispatch it and end hover mode
38
+ if (trigger) {
39
+ dispatch("click", { action: item });
40
+ active = null;
41
+ } else {
42
+ // do not remove active flag, to preserve the hover mode
43
+ active = -1;
44
+ }
45
+ }
46
+ }
47
+
48
+ function onHover(ev, item) {
49
+ if (active) setMenu(ev, item);
50
+ }
51
+ </script>
52
+
53
+ <div class="wx-menubar {css}">
54
+ {#each options as item (item.id)}
55
+ <button
56
+ class="wx-item {active === item.id ? 'wx-active' : ''}"
57
+ on:mouseenter={ev => onHover(ev, item)}
58
+ on:click={ev => setMenu(ev, item, true)}>{item.text}</button
59
+ >
60
+ {/each}
61
+ </div>
62
+
63
+ <ActionMenu
64
+ css={menuCss}
65
+ on:click={doClick}
66
+ options={menuOptions}
67
+ bind:handler={activate}
68
+ />
69
+
70
+ <style>
71
+ .wx-menubar {
72
+ display: flex;
73
+ position: relative;
74
+ width: fit-content;
75
+ }
76
+
77
+ .wx-item {
78
+ background-color: transparent;
79
+ border: none;
80
+ color: var(--wx-color-font);
81
+ box-sizing: border-box;
82
+ height: 36px;
83
+ line-height: 30px;
84
+ padding: 2px 12px;
85
+ font-family: var(--wx-font-family);
86
+ font-weight: var(--wx-font-weight);
87
+ font-size: var(--wx-font-size);
88
+
89
+ cursor: pointer;
90
+ outline: none;
91
+ white-space: nowrap;
92
+ }
93
+
94
+ .wx-active,
95
+ .wx-item:hover {
96
+ background-color: var(--wx-background-alt);
97
+ border-radius: var(--wx-button-border-radius);
98
+ }
99
+ </style>
@@ -0,0 +1,81 @@
1
+ <script>
2
+ import { getItemHandler } from "../helpers";
3
+
4
+ export let item;
5
+ export let showSub = false;
6
+ export let activeItem = null;
7
+
8
+ function onHover() {
9
+ showSub = item.data ? item.id : false;
10
+ activeItem = this;
11
+ }
12
+ </script>
13
+
14
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
15
+ <div
16
+ class="wx-item {item.css || ''}"
17
+ data-id={item.id}
18
+ on:mouseenter={onHover}
19
+ on:click
20
+ >
21
+ {#if item.icon}<i class="wx-icon {item.icon}" />{/if}
22
+ {#if item.type}
23
+ <svelte:component this={getItemHandler(item.type)} {item} />
24
+ {:else}<span class="wx-value"> {item.text} </span>{/if}
25
+ {#if item.subtext}<span class="wx-subtext">{item.subtext}</span>{/if}
26
+ {#if item.data}<i class="wx-sub-icon wxi-angle-right" />{/if}
27
+ </div>
28
+
29
+ <style>
30
+ .wx-item {
31
+ display: flex;
32
+ align-items: center;
33
+ box-sizing: border-box;
34
+ height: 36px;
35
+ line-height: 36px;
36
+ padding: 2px 12px;
37
+ font-family: var(--wx-font-family);
38
+ font-weight: var(--wx-font-weight);
39
+ font-size: var(--wx-font-size);
40
+ background-color: var(--wx-background);
41
+ cursor: pointer;
42
+ }
43
+
44
+ .wx-item:hover {
45
+ background: var(--wx-background-alt);
46
+ }
47
+
48
+ .wx-item:first-child {
49
+ border-top-left-radius: inherit;
50
+ border-top-right-radius: inherit;
51
+ }
52
+
53
+ .wx-item:last-child {
54
+ border-bottom-left-radius: inherit;
55
+ border-bottom-right-radius: inherit;
56
+ }
57
+
58
+ .wx-value {
59
+ flex-grow: 1;
60
+ white-space: nowrap;
61
+ color: var(--wx-color-font);
62
+ }
63
+ .wx-icon,
64
+ .wx-sub-icon {
65
+ vertical-align: middle;
66
+ height: inherit;
67
+ line-height: inherit;
68
+ font-size: var(--wx-icon-size);
69
+ color: var(--wx-icon-color);
70
+ }
71
+
72
+ .wx-icon {
73
+ margin-right: 8px;
74
+ }
75
+
76
+ .wx-subtext {
77
+ color: var(--wx-color-font-disabled);
78
+ margin-left: 20px;
79
+ white-space: nowrap;
80
+ }
81
+ </style>
@@ -0,0 +1,37 @@
1
+ export function walkData(data, cb) {
2
+ data.forEach(a => {
3
+ cb(a);
4
+ if (a.data && a.data.length) walkData(a.data, cb);
5
+ });
6
+ }
7
+
8
+ export function filterMenu(data, cb) {
9
+ const out = [];
10
+ data.forEach(a => {
11
+ if (a.data) {
12
+ const sub = filterMenu(a.data, cb);
13
+ if (sub.length) out.push({ ...a, data: sub });
14
+ } else {
15
+ if (cb(a)) out.push(a);
16
+ }
17
+ });
18
+
19
+ return out;
20
+ }
21
+
22
+ let uid = 1;
23
+ export function prepareMenuData(data) {
24
+ walkData(data, a => {
25
+ a.id = a.id || uid++;
26
+ });
27
+
28
+ return data;
29
+ }
30
+
31
+ const handlers = {};
32
+ export function getItemHandler(type) {
33
+ return handlers[type];
34
+ }
35
+ export function registerMenuItem(type, handler) {
36
+ handlers[type] = handler;
37
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import Menu from "./components/Menu.svelte";
2
+ import MenuBar from "./components/MenuBar.svelte";
3
+ import DropDownMenu from "./components/DropDownMenu.svelte";
4
+ import ContextMenu from "./components/ContextMenu.svelte";
5
+ import ActionMenu from "./components/ActionMenu.svelte";
6
+
7
+ export { registerMenuItem } from "./helpers";
8
+ export { Menu, MenuBar, DropDownMenu, ContextMenu, ActionMenu };
package/whatsnew.md ADDED
@@ -0,0 +1,72 @@
1
+ ### 1.3.0
2
+
3
+ - [dev] public release, using core@1.3.0
4
+
5
+ ### 1.2.4
6
+
7
+ - [update] ability to define custom css class for top menu and submenu's containers
8
+ - [fix] incorrect z-index when shown from popup
9
+
10
+ ### 1.2.3
11
+
12
+ - [update] when menu is closed it doesn't affect other popup elements
13
+ - [fix] regression in repositioning menu when clicking on next active area
14
+
15
+ ### 1.2.1
16
+
17
+ - [fix] regression in popup closing in some cases
18
+
19
+ ### 1.2.0
20
+
21
+ - [deps] uses core@1.2.0
22
+
23
+ ### 1.1.1
24
+
25
+ - [fix] incorrect positioning in "point" mode when menu initialized not as child of document.body
26
+ - [fix] submenus lost context values
27
+
28
+ ### 1.1.0
29
+
30
+ - [update] visual improvements
31
+ - [fix] incorrect auto-position logic
32
+
33
+ ### 0.0.1-rc21
34
+
35
+ - [fix] space between icon and text
36
+
37
+ ### 0.0.1-rc20
38
+
39
+ - [add] auto-fit for sub menus
40
+
41
+ ### 0.0.1-rc19
42
+
43
+ - [add] MenuBar - top level menu bar
44
+
45
+ ### 0.0.1-rc18
46
+
47
+ - menu doesn't require that all items have an unique IDs ( will generate missed ids on its own )
48
+
49
+ ### 0.0.1-rc17
50
+
51
+ - fix position in scrolled container
52
+ - temporary fix for cypress tests
53
+
54
+ ### 0.0.1-rc11
55
+
56
+ - updated styles
57
+ - bottom-fit mode
58
+
59
+ ### 0.0.1-rc10
60
+
61
+ #### Fixes
62
+
63
+ - incorrect position in scrollable container
64
+ - activation area can't contain complex markup inside
65
+ - missed hover styling
66
+
67
+ #### New features
68
+
69
+ - ability to init menu in some specific container
70
+ - ability to define subtext ( hotkey, etc. )
71
+ - ability to use custom components as menu items
72
+ - ability to use left and bottom-left position strategies