@matdata/yasqe 5.13.0 → 5.15.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasqe",
3
3
  "description": "Yet Another SPARQL Query Editor",
4
- "version": "5.13.0",
4
+ "version": "5.15.0",
5
5
  "main": "build/yasqe.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
package/src/index.ts CHANGED
@@ -61,6 +61,9 @@ export class Yasqe extends CodeMirror {
61
61
  private queryBtn: HTMLButtonElement | undefined;
62
62
  private saveBtn: HTMLButtonElement | undefined;
63
63
  private fullscreenBtn: HTMLButtonElement | undefined;
64
+ private hamburgerBtn: HTMLButtonElement | undefined;
65
+ private hamburgerMenu: HTMLDivElement | undefined;
66
+ private shareBtn: HTMLButtonElement | undefined;
64
67
  private isFullscreen: boolean = false;
65
68
  private horizontalResizeWrapper?: HTMLDivElement;
66
69
  private snippetsBar?: HTMLDivElement;
@@ -318,6 +321,7 @@ export class Yasqe extends CodeMirror {
318
321
  shareLinkWrapper.setAttribute("aria-label", "Share query");
319
322
  shareLinkWrapper.appendChild(shareIcon);
320
323
  buttons.appendChild(shareLinkWrapper);
324
+ this.shareBtn = shareLinkWrapper;
321
325
  shareLinkWrapper.addEventListener("click", (event: MouseEvent) => showSharePopup(event));
322
326
  shareLinkWrapper.addEventListener("keydown", (event: KeyboardEvent) => {
323
327
  if (event.code === "Enter") {
@@ -463,6 +467,7 @@ export class Yasqe extends CodeMirror {
463
467
  urlBtn.className = "yasqe_btn yasqe_btn-sm yasqe_shareBtn";
464
468
  buttonContainer.appendChild(urlBtn);
465
469
  urlBtn.onclick = async () => {
470
+ if (!this.config.createShareableLink) return;
466
471
  const url = this.config.createShareableLink(this);
467
472
  await copyToClipboard(url, "URL");
468
473
  };
@@ -478,6 +483,7 @@ export class Yasqe extends CodeMirror {
478
483
  shortenBtn.disabled = true;
479
484
  shortenBtn.innerText = "Shortening...";
480
485
  try {
486
+ if (!this.config.createShareableLink) return;
481
487
  const longUrl = this.config.createShareableLink(this);
482
488
  const shortUrl = await createShortLink(this, longUrl);
483
489
  await copyToClipboard(shortUrl, "Shortened URL");
@@ -533,9 +539,25 @@ export class Yasqe extends CodeMirror {
533
539
  // Position popup after layout is complete
534
540
  const positionPopup = () => {
535
541
  if (!popup) return;
536
- const sharePos = shareLinkWrapper.getBoundingClientRect();
537
- popup.style.top = shareLinkWrapper.offsetTop + sharePos.height + "px";
538
- popup.style.left = shareLinkWrapper.offsetLeft + shareLinkWrapper.clientWidth - popup.clientWidth + "px";
542
+ const buttonsRect = buttons.getBoundingClientRect();
543
+ const popupHeight = popup.offsetHeight || 300; // Estimated height
544
+ const viewportHeight = window.innerHeight;
545
+ const spaceAbove = buttonsRect.top;
546
+ const spaceBelow = viewportHeight - buttonsRect.bottom;
547
+
548
+ // If there's enough space above (with some padding), position above
549
+ // Otherwise position below (will use scrolling if needed)
550
+ if (spaceAbove >= popupHeight + 20) {
551
+ popup.style.bottom = (buttons.clientHeight || 46) + "px";
552
+ popup.style.top = "auto";
553
+ popup.style.right = "0px";
554
+ } else {
555
+ // Not enough space above, use fixed positioning at top of viewport
556
+ popup.style.position = "fixed";
557
+ popup.style.bottom = "auto";
558
+ popup.style.top = "20px";
559
+ popup.style.right = "20px";
560
+ }
539
561
  };
540
562
 
541
563
  if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
@@ -609,6 +631,130 @@ export class Yasqe extends CodeMirror {
609
631
  this.fullscreenBtn.title = "Toggle fullscreen (F11)";
610
632
  this.fullscreenBtn.setAttribute("aria-label", "Toggle fullscreen");
611
633
  buttons.appendChild(this.fullscreenBtn);
634
+
635
+ /**
636
+ * Draw hamburger menu button and dropdown (for mobile)
637
+ */
638
+ this.hamburgerBtn = document.createElement("button");
639
+ addClass(this.hamburgerBtn, "yasqe_hamburgerButton");
640
+ const hamburgerIcon = document.createElement("i");
641
+ addClass(hamburgerIcon, "fas");
642
+ addClass(hamburgerIcon, "fa-bars");
643
+ hamburgerIcon.setAttribute("aria-hidden", "true");
644
+ this.hamburgerBtn.appendChild(hamburgerIcon);
645
+ this.hamburgerBtn.title = "More options";
646
+ this.hamburgerBtn.setAttribute("aria-label", "More options");
647
+ this.hamburgerBtn.setAttribute("aria-expanded", "false");
648
+ buttons.appendChild(this.hamburgerBtn);
649
+
650
+ // Create hamburger menu
651
+ this.hamburgerMenu = document.createElement("div");
652
+ this.hamburgerMenu.className = "yasqe_hamburgerMenu";
653
+ buttons.appendChild(this.hamburgerMenu);
654
+
655
+ // Add menu items
656
+ if (this.config.createShareableLink) {
657
+ const shareItem = document.createElement("button");
658
+ shareItem.className = "yasqe_hamburgerMenuItem";
659
+ const shareIconMenu = document.createElement("i");
660
+ addClass(shareIconMenu, "fas");
661
+ addClass(shareIconMenu, "fa-share-nodes");
662
+ shareIconMenu.setAttribute("aria-hidden", "true");
663
+ shareItem.appendChild(shareIconMenu);
664
+ const shareLabel = document.createElement("span");
665
+ shareLabel.textContent = "Share";
666
+ shareItem.appendChild(shareLabel);
667
+ shareItem.onclick = () => {
668
+ this.closeHamburgerMenu();
669
+ this.shareBtn?.click();
670
+ };
671
+ this.hamburgerMenu.appendChild(shareItem);
672
+ }
673
+
674
+ const saveItem = document.createElement("button");
675
+ saveItem.className = "yasqe_hamburgerMenuItem";
676
+ const saveIconMenu = document.createElement("i");
677
+ addClass(saveIconMenu, "fas");
678
+ addClass(saveIconMenu, "fa-save");
679
+ saveIconMenu.setAttribute("aria-hidden", "true");
680
+ saveItem.appendChild(saveIconMenu);
681
+ const saveLabel = document.createElement("span");
682
+ saveLabel.textContent = "Save";
683
+ saveItem.appendChild(saveLabel);
684
+ saveItem.onclick = () => {
685
+ this.closeHamburgerMenu();
686
+ this.emit("saveManagedQuery");
687
+ };
688
+ this.hamburgerMenu.appendChild(saveItem);
689
+
690
+ if (this.config.showFormatButton) {
691
+ const formatItem = document.createElement("button");
692
+ formatItem.className = "yasqe_hamburgerMenuItem";
693
+ const formatIconMenu = document.createElement("i");
694
+ addClass(formatIconMenu, "fas");
695
+ addClass(formatIconMenu, "fa-align-left");
696
+ formatIconMenu.setAttribute("aria-hidden", "true");
697
+ formatItem.appendChild(formatIconMenu);
698
+ const formatLabel = document.createElement("span");
699
+ formatLabel.textContent = "Format";
700
+ formatItem.appendChild(formatLabel);
701
+ formatItem.onclick = () => {
702
+ this.closeHamburgerMenu();
703
+ this.format();
704
+ };
705
+ this.hamburgerMenu.appendChild(formatItem);
706
+ }
707
+
708
+ const fullscreenItem = document.createElement("button");
709
+ fullscreenItem.className = "yasqe_hamburgerMenuItem";
710
+ const fullscreenIconMenu = document.createElement("i");
711
+ addClass(fullscreenIconMenu, "fas");
712
+ addClass(fullscreenIconMenu, "fa-expand");
713
+ fullscreenIconMenu.setAttribute("aria-hidden", "true");
714
+ fullscreenItem.appendChild(fullscreenIconMenu);
715
+ const fullscreenLabel = document.createElement("span");
716
+ fullscreenLabel.textContent = "Fullscreen";
717
+ fullscreenItem.appendChild(fullscreenLabel);
718
+ fullscreenItem.onclick = () => {
719
+ this.closeHamburgerMenu();
720
+ this.toggleFullscreen();
721
+ };
722
+ this.hamburgerMenu.appendChild(fullscreenItem);
723
+
724
+ // Toggle hamburger menu
725
+ this.hamburgerBtn.onclick = (e) => {
726
+ e.stopPropagation();
727
+ this.toggleHamburgerMenu();
728
+ };
729
+
730
+ // Close hamburger menu when clicking outside
731
+ document.addEventListener("click", (e) => {
732
+ if (
733
+ this.hamburgerMenu &&
734
+ this.hamburgerBtn &&
735
+ !this.hamburgerMenu.contains(e.target as Node) &&
736
+ !this.hamburgerBtn.contains(e.target as Node)
737
+ ) {
738
+ this.closeHamburgerMenu();
739
+ }
740
+ });
741
+ }
742
+
743
+ private toggleHamburgerMenu() {
744
+ if (!this.hamburgerMenu || !this.hamburgerBtn) return;
745
+ const isActive = this.hamburgerMenu.classList.contains("active");
746
+ if (isActive) {
747
+ this.closeHamburgerMenu();
748
+ } else {
749
+ addClass(this.hamburgerMenu, "active");
750
+ this.hamburgerBtn.setAttribute("aria-expanded", "true");
751
+ }
752
+ }
753
+
754
+ private closeHamburgerMenu() {
755
+ if (!this.hamburgerMenu || !this.hamburgerBtn) return;
756
+ removeClass(this.hamburgerMenu, "active");
757
+ this.hamburgerBtn.setAttribute("aria-expanded", "false");
612
758
  }
613
759
  public toggleFullscreen() {
614
760
  this.isFullscreen = !this.isFullscreen;
@@ -1830,7 +1976,7 @@ export interface Config extends Partial<CodeMirror.EditorConfiguration> {
1830
1976
  * By default, this feature is enabled, and the only the query value is appended to the link.
1831
1977
  * ps. This function should return an object which is parseable by jQuery.param (http://api.jquery.com/jQuery.param/)
1832
1978
  */
1833
- createShareableLink: (yasqe: Yasqe) => string;
1979
+ createShareableLink: ((yasqe: Yasqe) => string) | undefined | null;
1834
1980
  createShortLink: ((yasqe: Yasqe, longLink: string) => Promise<string>) | undefined;
1835
1981
  consumeShareLink: ((yasqe: Yasqe) => void) | undefined | null;
1836
1982
  /**
@@ -3,10 +3,10 @@
3
3
  $queryButtonHeight: 40px;
4
4
 
5
5
  .yasqe_btn {
6
- color: #333;
6
+ color: var(--yasgui-text-primary, #333);
7
7
  border: 1px solid transparent;
8
- background-color: #fff;
9
- border-color: #ccc;
8
+ background-color: var(--yasgui-bg-primary, #fff);
9
+ border-color: var(--yasgui-border-color, #ccc);
10
10
  border-width: 1px;
11
11
  display: inline-block;
12
12
  text-align: center;
@@ -33,8 +33,8 @@
33
33
 
34
34
  &:hover {
35
35
  outline: 0;
36
- background-color: #ebebeb;
37
- border-color: #adadad;
36
+ background-color: var(--yasgui-bg-secondary, #ebebeb);
37
+ border-color: var(--yasgui-text-muted, #adadad);
38
38
  }
39
39
 
40
40
  &:focus,
@@ -46,10 +46,10 @@
46
46
  }
47
47
 
48
48
  &.btn_icon:focus {
49
- color: #333;
49
+ color: var(--yasgui-text-primary, #333);
50
50
  border: 1px solid transparent;
51
- background-color: #fff;
52
- border-color: #ccc;
51
+ background-color: var(--yasgui-bg-primary, #fff);
52
+ border-color: var(--yasgui-border-color, #ccc);
53
53
  }
54
54
 
55
55
  &.yasqe_btn-sm {
@@ -62,7 +62,7 @@
62
62
 
63
63
  .yasqe_buttons {
64
64
  position: absolute;
65
- top: 10px;
65
+ bottom: 10px;
66
66
  right: 20px;
67
67
  z-index: 5;
68
68
 
@@ -77,7 +77,7 @@
77
77
  margin-left: 8px;
78
78
  height: 36px;
79
79
  width: 36px;
80
- color: #505050;
80
+ color: var(--yasgui-text-secondary, #505050);
81
81
 
82
82
  i {
83
83
  font-size: 20px;
@@ -96,22 +96,25 @@
96
96
  position: absolute;
97
97
  padding: 12px;
98
98
  margin-left: 0px;
99
- background-color: #fff;
100
- border: 1px solid #e3e3e3;
99
+ background-color: var(--yasgui-bg-primary, #fff);
100
+ border: 1px solid var(--yasgui-border-color, #e3e3e3);
101
101
  border-radius: 4px;
102
102
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
103
103
  width: auto;
104
104
  min-width: 300px;
105
105
  max-width: 400px;
106
+ max-height: calc(100vh - 100px);
107
+ overflow-y: auto;
106
108
  height: auto;
107
109
  display: flex;
108
110
  flex-direction: column;
109
111
  gap: 8px;
112
+ z-index: 10001;
110
113
 
111
114
  .yasqe_sharePopup_title {
112
115
  font-weight: 600;
113
116
  font-size: 14px;
114
- color: #333;
117
+ color: var(--yasgui-text-primary, #333);
115
118
  margin-bottom: 4px;
116
119
  }
117
120
 
@@ -301,7 +304,7 @@
301
304
  margin-left: 8px;
302
305
  height: 36px;
303
306
  width: 36px;
304
- color: #505050;
307
+ color: var(--yasgui-text-secondary, #505050);
305
308
 
306
309
  i {
307
310
  font-size: 20px;
@@ -323,7 +326,7 @@
323
326
  margin-left: 8px;
324
327
  height: 36px;
325
328
  width: 36px;
326
- color: #505050;
329
+ color: var(--yasgui-text-secondary, #505050);
327
330
 
328
331
  i {
329
332
  font-size: 20px;
@@ -345,7 +348,7 @@
345
348
  margin-left: 8px;
346
349
  height: 36px;
347
350
  width: 36px;
348
- color: #505050;
351
+ color: var(--yasgui-text-secondary, #505050);
349
352
 
350
353
  i {
351
354
  font-size: 20px;
@@ -359,6 +362,95 @@
359
362
  color: #337ab7;
360
363
  }
361
364
  }
365
+
366
+ .yasqe_hamburgerButton {
367
+ display: none;
368
+ align-items: center;
369
+ justify-content: center;
370
+ border: none;
371
+ background: none;
372
+ cursor: pointer;
373
+ padding: 6px;
374
+ margin-left: 8px;
375
+ height: 36px;
376
+ width: 36px;
377
+ color: var(--yasgui-text-secondary, #505050);
378
+
379
+ i {
380
+ font-size: 20px;
381
+ }
382
+
383
+ &:hover {
384
+ color: #337ab7;
385
+ }
386
+
387
+ &[aria-expanded="true"] {
388
+ color: #337ab7;
389
+ }
390
+ }
391
+
392
+ .yasqe_hamburgerMenu {
393
+ display: none;
394
+ position: absolute;
395
+ bottom: 46px;
396
+ right: 0;
397
+ background-color: var(--yasgui-bg-primary, #fff);
398
+ border: 1px solid var(--yasgui-border-color, #e3e3e3);
399
+ border-radius: 4px;
400
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
401
+ padding: 4px;
402
+ min-width: 180px;
403
+ z-index: 10000;
404
+
405
+ &.active {
406
+ display: block;
407
+ }
408
+
409
+ .yasqe_hamburgerMenuItem {
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 10px;
413
+ width: 100%;
414
+ padding: 10px 12px;
415
+ border: none;
416
+ background: none;
417
+ text-align: left;
418
+ cursor: pointer;
419
+ font-size: 14px;
420
+ color: var(--yasgui-text-primary, #333);
421
+ border-radius: 2px;
422
+
423
+ i {
424
+ font-size: 16px;
425
+ width: 20px;
426
+ color: var(--yasgui-text-secondary, #505050);
427
+ }
428
+
429
+ &:hover {
430
+ background-color: var(--yasgui-bg-secondary, #f0f0f0);
431
+ }
432
+
433
+ &:active {
434
+ background-color: var(--yasgui-bg-tertiary, #e0e0e0);
435
+ }
436
+ }
437
+ }
438
+ }
439
+
440
+ // Responsive: Show hamburger menu on smaller screens
441
+ @media (max-width: 768px) {
442
+ .yasqe_buttons {
443
+ .yasqe_share,
444
+ .yasqe_saveButton,
445
+ .yasqe_formatButton,
446
+ .yasqe_fullscreenButton {
447
+ display: none !important;
448
+ }
449
+
450
+ .yasqe_hamburgerButton {
451
+ display: inline-flex;
452
+ }
453
+ }
362
454
  }
363
455
 
364
456
  &.fullscreen {
@@ -368,7 +460,7 @@
368
460
  right: 0;
369
461
  bottom: 0;
370
462
  z-index: 9998;
371
- background: white;
463
+ background: var(--yasgui-bg-primary, white);
372
464
  margin: 0;
373
465
  padding-top: 100px;
374
466
 
@@ -378,7 +470,7 @@
378
470
 
379
471
  .yasqe_buttons {
380
472
  position: fixed;
381
- top: 110px;
473
+ bottom: 40px;
382
474
  right: 20px;
383
475
  z-index: 10000;
384
476
  }