@zekibu/ctxmenu 0.1.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.
Files changed (3) hide show
  1. package/index.d.ts +36 -0
  2. package/index.js +415 -0
  3. package/package.json +11 -0
package/index.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ interface MenuOption {
2
+ /**
3
+ * 菜单项显示的文本
4
+ */
5
+ name: string;
6
+ /**
7
+ * Font Awesome 图标类名,如果不需要图标则为 false
8
+ */
9
+ icon?: string | false;
10
+ /**
11
+ * 是否在右侧显示,默认 false
12
+ */
13
+ right?: boolean;
14
+ /**
15
+ * 点击菜单项时的回调函数
16
+ */
17
+ click?: ((e: MouseEvent, target: HTMLElement) => void) | false;
18
+ /**
19
+ * 自定义样式
20
+ */
21
+ style?: string | false;
22
+ /**
23
+ * 是否禁用该菜单项,默认 false
24
+ */
25
+ disabled?: boolean;
26
+ }
27
+
28
+ /**
29
+ * 初始化右键菜单
30
+ * @param el - 目标元素或选择器
31
+ * @param option - 菜单选项数组
32
+ * @param close - 菜单关闭时的回调函数
33
+ */
34
+ declare function init(el: string | HTMLElement, option: (MenuOption | string)[], close?: () => void): void;
35
+
36
+ export default init;
package/index.js ADDED
@@ -0,0 +1,415 @@
1
+ /*
2
+ * @Author: ZekiNotFound <root@zekibu.im>
3
+ * @Description:鼠标右键菜单
4
+ */
5
+
6
+ const SRC_FA = '//cdn.bootcdn.net/ajax/libs/font-awesome/6.4.2/css/all.min.css';
7
+ export default init = (el, option = [], close = () => { }) => {
8
+ if (!el) return console.error('目标元素不存在');
9
+ el = typeof el != 'string' ? el : document.querySelector(el);
10
+ if (!el) return console.error('目标元素不存在');
11
+ if (option.length <= 0) return;
12
+ let options = [],
13
+ i = 0;
14
+ for (let v of option) {
15
+ if (typeof v == 'string') {
16
+ options[i] = {
17
+ name: v,
18
+ icon: false,
19
+ right: false,
20
+ click: false,
21
+ style: false,
22
+ disabled: false
23
+ }
24
+ } else {
25
+ options[i] = {
26
+ name: typeof v.name == 'undefined' ? '' : v.name,
27
+ icon: typeof v.icon == 'undefined' ? false : v.icon,
28
+ right: typeof v.right == 'undefined' ? false : v.right,
29
+ click: typeof v.click != 'function' ? false : v.click,
30
+ style: typeof v.style == 'undefined' ? false : v.style,
31
+ disabled: typeof v.disabled == 'undefined' ? false : v.disabled
32
+ }
33
+ }
34
+ // 如果有任何一个icon则加载fa图标库
35
+ if (options[i].icon !== false && options[i].icon != '') {
36
+ loadCss(SRC_FA);
37
+ }
38
+ i++;
39
+ }
40
+
41
+ el.oncontextmenu = event => {
42
+ event.preventDefault();
43
+ event.stopPropagation();
44
+ const { clientX, clientY } = event;
45
+ let index = (new Date).getTime();
46
+ el.dataset.ctxmenuIndex = index;
47
+ let box = createDom(index, options, clientX, clientY, event.target, getSelect());
48
+ if (el.dataset.ctxmenuIndexOld != undefined && el.dataset.ctxmenuIndexOld != index) {
49
+ let dom = document.querySelector(`[data-index="${el.dataset.ctxmenuIndexOld}"]`);
50
+ if (dom) {
51
+ dom.remove();
52
+ if (typeof close == 'function') close();
53
+ }
54
+ }
55
+ el.dataset.ctxmenuIndexOld = box.dataset.index;
56
+ }
57
+ };
58
+
59
+ /**
60
+ * @description: 创建Dom
61
+ * @param {*} index
62
+ * @param {*} options
63
+ * @param {*} x
64
+ * @param {*} y
65
+ * @param {*} target
66
+ * @param {*} select
67
+ * @return {*}
68
+ */
69
+ function createDom(index, options, x, y, target, select) {
70
+ let box = document.createElement('div'),
71
+ onclickOld = document.body.onclick || (() => { }),
72
+ i = 0,
73
+ fun = [];
74
+ box.className = 'zk-ctxmenu zk-ctxmenu-in';
75
+ box.id = 'zk_ctxmenu_' + document.querySelectorAll('.zk-ctxmenu').length.toString();
76
+ box.dataset.index = index;
77
+ box.style.display = 'none';
78
+ box.oncontextmenu = event => {
79
+ event.preventDefault();
80
+ event.stopPropagation();
81
+ };
82
+ document.body.appendChild(box);
83
+ for (let v of options) {
84
+ let name, icon, right, style = '';
85
+ let item = document.createElement('div');
86
+ let rightBox = null, rightItem = '', clickFun = [];
87
+ if (v.disabled !== false) {
88
+ item.className = 'zk-ctxmenu-item zk-ctxmenu-disabled';
89
+ if (typeof v.disabled == 'string') {
90
+ item.title = v.disabled;
91
+ }
92
+ } else {
93
+ item.className = 'zk-ctxmenu-item';
94
+ }
95
+ if (v.name == '') {
96
+ name = `<span class="zk-ctxmenu-name"></span>`;
97
+ item.className += ' zk-ctxmenu-no-hover';
98
+ } else if (v.name == '-') {
99
+ name = `<span class="zk-ctxmenu-name"><hr/></span>`;
100
+ item.className += ' zk-ctxmenu-no-hover';
101
+ } else {
102
+ if (v.name.length > 20) v.name = v.name.substr(0, 20) + '...';
103
+ name = `<span class="zk-ctxmenu-name">${v.name}</span>`;
104
+ }
105
+ if (v.icon === false || v.icon == '') {
106
+ icon = '<i></i>';
107
+ } else {
108
+ icon = `<i class="zk-ctxmenu-icon fa-solid fa-${v.icon}"></i>`;
109
+ }
110
+ if (v.right === false || v.right == '') {
111
+ right = `<span></span>`;
112
+ } else if (typeof v.right == 'string') {
113
+ right = `<span class="zk-ctxmenu-right">${v.right}</span>`;
114
+ } else if (typeof v.right == 'object') {
115
+ let rightDom = document.createElement('i');
116
+ rightBox = document.createElement('ul');
117
+ rightBox.className = 'zk-ctxmenu-ul';
118
+ rightBox.style.display = 'none';
119
+ rightBox.dataset.show = 'false';
120
+ rightDom.className = 'zk-ctxmenu-right fa-solid fa-angle-right';
121
+ if (v.right.length != undefined) {
122
+ let _right = {};
123
+ for (let k in v.right) _right[v.right[k]] = '';
124
+ v.right = _right;
125
+ }
126
+ for (let k in v.right) {
127
+ let _rightItem = document.createElement('li');
128
+ _rightItem.className = 'zk-ctxmenu-li';
129
+ _rightItem.dataset.funIndex = k;
130
+ _rightItem.innerText = k.length > 9 ? k.substring(0, 9) + '...' : k;
131
+ rightItem += _rightItem.outerHTML;
132
+ if (typeof v.right[k] == 'function') {
133
+ clickFun[k] = v.right[k];
134
+ }
135
+ }
136
+ rightBox.innerHTML = rightItem;
137
+ right = rightDom.outerHTML;
138
+ } else {
139
+ right = '';
140
+ }
141
+ if (v.style === false) {
142
+ style = '';
143
+ }
144
+ if (typeof v.style == 'function') {
145
+ v.style = v.style();
146
+ }
147
+ if (typeof v.style == 'string') {
148
+ style = v.style;
149
+ }
150
+ if (typeof v.style == 'object') {
151
+ style = '';
152
+ for (let k in v.style) {
153
+ item.style[k] = v.style[k];
154
+ }
155
+ }
156
+ item.innerHTML = icon + name + right;
157
+ if (style != '') item.style.cssText = style;
158
+ item.onclick = event => {
159
+ if (event.target && event.target.nodeName === 'LI') {
160
+ let _isClose = true;
161
+ if (typeof clickFun[event.target.dataset.funIndex] == 'function') {
162
+ _isClose = clickFun[event.target.dataset.funIndex]();
163
+ }
164
+ if (_isClose !== false) box.remove();
165
+
166
+ return;
167
+ }
168
+ let isClose = true;
169
+ if (typeof v.click == 'function' && item.className.indexOf('zk-ctxmenu-disabled') == -1) {
170
+ isClose = v.click(target, select);
171
+ }
172
+ if (isClose !== false && item.className.indexOf('zk-ctxmenu-disabled') == -1) box.remove();
173
+ }
174
+ box.appendChild(item);
175
+ if (rightBox && rightItem != '') {
176
+ item.appendChild(rightBox);
177
+ fun[i] = () => {
178
+ let itemBoxObj = item.getBoundingClientRect();
179
+ rightBox.style.left = itemBoxObj.width + 'px';
180
+ let rightBoxObj = rightBox.getBoundingClientRect(),
181
+ rightX = itemBoxObj.x + itemBoxObj.width - 2,
182
+ rightY = itemBoxObj.y - 10;
183
+ if (rightX + rightBoxObj.width > document.body.offsetWidth) {
184
+ rightX = rightX - itemBoxObj.width - rightBoxObj.width + 2;
185
+ }
186
+ if (rightY + rightBoxObj.height > document.body.offsetHeight) {
187
+ rightY = rightY + itemBoxObj.height - rightBoxObj.height + 20;
188
+ }
189
+
190
+ rightBox.style.left = rightX + 'px';
191
+ rightBox.style.top = rightY + 'px';
192
+ }
193
+
194
+ let isIn = false;
195
+ item.onmouseover = () => {
196
+ rightBox.style.display = 'block';
197
+ rightBox.style.zIndex += document.querySelectorAll('.zk-ctxmenu-ul[data-show="true"]').length;
198
+ rightBox.dataset.show = 'true';
199
+ isIn = true;
200
+ }
201
+ item.onmouseout = () => {
202
+ setTimeout(() => {
203
+ if (!isIn) {
204
+ rightBox.style.display = 'none';
205
+ rightBox.dataset.show = 'false';
206
+ }
207
+ }, 500);
208
+ isIn = false;
209
+ }
210
+ }
211
+ i++;
212
+ }
213
+ if (x + box.offsetWidth > document.body.offsetWidth) {
214
+ x = x - box.offsetWidth;
215
+ }
216
+ if (y + box.offsetHeight > document.body.offsetHeight) {
217
+ y = y - box.offsetHeight;
218
+ }
219
+ box.style.top = y + 'px';
220
+ box.style.left = x + 'px';
221
+ for (let v of fun) {
222
+ if (typeof v == 'function') v();
223
+ }
224
+ document.body.onclick = event => {
225
+ onclickOld();
226
+ if (event && event.target != box && event.target.className.indexOf('zk-ctxmenu') == -1) {
227
+ box.remove();
228
+ document.body.onclick = onclickOld;
229
+ }
230
+ }
231
+
232
+ return box;
233
+ }
234
+
235
+ /**
236
+ * @description: 获取选中的文本
237
+ * @return {*}
238
+ */
239
+ function getSelect() {
240
+ let selectedText = '';
241
+ if (window.getSelection) {
242
+ selectedText = window.getSelection().toString();
243
+ } else if (document.selection && document.selection.type != 'Control') {
244
+ selectedText = document.selection.createRange().text;
245
+ }
246
+ return selectedText;
247
+ }
248
+ function createElement(tagName, attr = {}) {
249
+ if (!tagName) {
250
+ throw new Error('[tagName] 不能为空');
251
+ }
252
+ const tag = document.createElement(tagName);
253
+
254
+ setAttr(tag, attr);
255
+ return tag;
256
+ };
257
+ function setAttr(tag, attr = {}) {
258
+ for (let k in attr) {
259
+ if (typeof attr[k] === 'object' && !Array.isArray(attr[k])) {
260
+ setAttr(tag[k], attr[k]);
261
+ } else if (k in tag) {
262
+ tag[k] = (!k.startsWith('on') && typeof attr[k] === 'function') ? attr[k](tag, attr, k) : attr[k];
263
+ } else if (typeof attr[k] === 'function') {
264
+ tag.setAttribute(k, attr[k]());
265
+ } else {
266
+ try {
267
+ tag.setAttribute(k, attr[k]);
268
+ } catch (e) {
269
+ tag[k] = attr[k];
270
+ }
271
+ }
272
+ }
273
+ }
274
+ const head = document.querySelector('head');
275
+ function loadCss(href) {
276
+ const style = createElement('style', {
277
+ type: 'text/css',
278
+ rel: 'stylesheet',
279
+ 'data-npm': '@zekibu/ctxmenu',
280
+ href: href
281
+ });
282
+ head.appendChild(style);
283
+ }
284
+
285
+ const CSS = `
286
+ .zk-ctxmenu {
287
+ position: fixed;
288
+ z-index: 19991111;
289
+ min-width: 200px;
290
+ max-height: 80vh;
291
+ max-width: 80vw;
292
+ border: 1px solid #eee;
293
+ background-color: #fff;
294
+ box-shadow: 1px 1px 10px rgba(0, 0, 0, .2);
295
+ border-radius: 2px;
296
+ display: block !important;
297
+ padding: 10px 0;
298
+ user-select: none;
299
+ --webkit-user-select: none;
300
+ }
301
+
302
+ .zk-ctxmenu-in {
303
+ animation-name: fadeIn;
304
+ animation-fill-mode: both;
305
+ animation-duration: .1s
306
+ }
307
+
308
+ @keyframes fadeIn {
309
+ 0% {
310
+ opacity: 0;
311
+ /* transform: scale(.5); */
312
+ }
313
+
314
+ 100% {
315
+ opacity: 1;
316
+ /* transform: scale(1); */
317
+ }
318
+ }
319
+
320
+ .zk-ctxmenu-item {
321
+ width: 100%;
322
+ height: 35px;
323
+ line-height: 35px;
324
+ text-overflow: ellipsis;
325
+ white-space: nowrap;
326
+ display: grid;
327
+ grid-template-columns: 40px auto 100px;
328
+ align-items: center;
329
+ position: relative;
330
+ }
331
+
332
+ .zk-ctxmenu-disabled {
333
+ cursor: not-allowed !important;
334
+ background-color: #f2f2f2;
335
+ }
336
+
337
+ .zk-ctxmenu-disabled>* {
338
+ color: #8d8d8d;
339
+ }
340
+
341
+ .zk-ctxmenu-item.zk-ctxmenu-no-hover {
342
+ height: 20px;
343
+ }
344
+
345
+ .zk-ctxmenu-item:not(.zk-ctxmenu-no-hover) {
346
+ cursor: pointer;
347
+ }
348
+
349
+ .zk-ctxmenu-item:not(.zk-ctxmenu-no-hover):not(.zk-ctxmenu-disabled):hover,
350
+ .zk-ctxmenu-li:hover {
351
+ background-color: #F2F2F2;
352
+ }
353
+
354
+ .zk-ctxmenu-name,
355
+ .zk-ctxmenu-li {
356
+ display: flex;
357
+ align-items: center;
358
+ font-size: 13px;
359
+ min-width: none;
360
+ overflow: hidden;
361
+ text-overflow: ellipsis;
362
+ }
363
+
364
+ .zk-ctxmenu-icon {
365
+ justify-self: center;
366
+ font-size: 14px;
367
+ color: #444;
368
+ }
369
+
370
+ .zk-ctxmenu-right {
371
+ justify-self: end;
372
+ padding-right: 15px;
373
+ font-size: 12px;
374
+ color: #777;
375
+ }
376
+
377
+ .zk-ctxmenu-name>hr {
378
+ position: absolute;
379
+ width: 100%;
380
+ height: 1px;
381
+ background-color: #D3E3FD;
382
+ border: none;
383
+ left: 0;
384
+ transform: scaleY(0.5);
385
+ }
386
+
387
+ .zk-ctxmenu-ul {
388
+ background-color: #fff;
389
+ position: fixed;
390
+ z-index: 19991111;
391
+ border: 1px solid #eee;
392
+ background-color: #fff;
393
+ box-shadow: 1px 1px 10px rgba(0, 0, 0, .2);
394
+ padding: 10px 0;
395
+ margin: 0;
396
+ list-style: none;
397
+ border-radius: 5px;
398
+ max-width: 150px;
399
+ overflow: hidden;
400
+ transition: opacity .2s;
401
+ }
402
+
403
+ .zk-ctxmenu-li {
404
+ min-width: 0;
405
+ padding: 0 15px;
406
+ max-width: 80%;
407
+ overflow: hidden;
408
+ text-overflow: ellipsis;
409
+ }
410
+ `;
411
+ const style = createElement('style', {
412
+ innerText: CSS,
413
+ 'data-npm': '@zekibu/ctxmenu'
414
+ });
415
+ head.appendChild(style);
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@zekibu/ctxmenu",
3
+ "version": "0.1.0",
4
+ "description": "轻量化右键菜单",
5
+ "main": "./index.js",
6
+ "type": "module",
7
+ "types": "./index.d.ts",
8
+ "keywords": [],
9
+ "author": "ZekiNotFound <root@zekibu.im>",
10
+ "license": "ISC"
11
+ }