From 83dfb6de85588c4c36bd3bcc1d42cbe2ec122aa8 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:53:27 -0800 Subject: [PATCH] merge igv-widgets source into project --- css/_file-load-widget.scss | 366 ++++++ css/_igv-widgets-alert-dialog.scss | 143 +++ css/app.css | 318 +++++- css/app.css.map | 2 +- css/app.scss | 8 + js/app.js | 25 +- js/genomeWidgets.js | 12 +- js/shareHelper.js | 2 +- js/shareWidgets.js | 3 +- js/widgets/alertDialog.js | 92 ++ js/widgets/alertSingleton.js | 21 + .../encodeTrackDatasourceConfigurator.js | 190 ++++ js/widgets/eventBus.js | 43 + js/widgets/fileLoad.js | 88 ++ js/widgets/fileLoadManager.js | 62 + js/widgets/fileLoadWidget.js | 224 ++++ js/widgets/genericSelectModal.js | 35 + js/widgets/genomeFileLoad.js | 75 ++ js/widgets/markupFactory.js | 44 + js/widgets/multipleTrackFileLoad.js | 181 +++ js/widgets/qrcode.js | 1000 +++++++++++++++++ js/widgets/sessionController.js | 79 ++ js/widgets/sessionFileLoad.js | 24 + js/widgets/sessionWidgets.js | 150 +++ js/widgets/trackURLModal.js | 39 + js/widgets/trackWidgets.js | 434 +++++++ js/widgets/urlModal.js | 38 + js/widgets/utils.js | 40 + js/widgets/utils/dom-utils.js | 120 ++ js/{ => widgets/utils}/draggable.js | 24 +- js/widgets/utils/icons.js | 48 + js/widgets/utils/ui-utils.js | 16 + package.json | 3 +- 33 files changed, 3909 insertions(+), 40 deletions(-) create mode 100644 css/_file-load-widget.scss create mode 100644 css/_igv-widgets-alert-dialog.scss create mode 100644 js/widgets/alertDialog.js create mode 100644 js/widgets/alertSingleton.js create mode 100644 js/widgets/encodeTrackDatasourceConfigurator.js create mode 100644 js/widgets/eventBus.js create mode 100644 js/widgets/fileLoad.js create mode 100644 js/widgets/fileLoadManager.js create mode 100644 js/widgets/fileLoadWidget.js create mode 100644 js/widgets/genericSelectModal.js create mode 100644 js/widgets/genomeFileLoad.js create mode 100644 js/widgets/markupFactory.js create mode 100644 js/widgets/multipleTrackFileLoad.js create mode 100755 js/widgets/qrcode.js create mode 100644 js/widgets/sessionController.js create mode 100644 js/widgets/sessionFileLoad.js create mode 100644 js/widgets/sessionWidgets.js create mode 100644 js/widgets/trackURLModal.js create mode 100644 js/widgets/trackWidgets.js create mode 100644 js/widgets/urlModal.js create mode 100644 js/widgets/utils.js create mode 100644 js/widgets/utils/dom-utils.js rename js/{ => widgets/utils}/draggable.js (77%) create mode 100644 js/widgets/utils/icons.js create mode 100644 js/widgets/utils/ui-utils.js diff --git a/css/_file-load-widget.scss b/css/_file-load-widget.scss new file mode 100644 index 00000000..d12a8a12 --- /dev/null +++ b/css/_file-load-widget.scss @@ -0,0 +1,366 @@ +$igv-flw-font-face: 'Open Sans', sans-serif; +$igv-flw-font-size: .875rem; +$igv-flw-light-grey-color: #bfbfbf; +$igv-flw-grey-color: #7F7F7F; +$igv-flw-dark-grey-color: #373737; + +$igv-flw-label-font-color: #242424; +$igv-flw-outline-grey-color: #7c7c7c; + +$igv-flw-modal-button-dark-color: #6c757c; + +$igv-flw-button-ok-color: #5ea4e0; +$igv-flw-button-ok-hover-color: #3b5c7f; +$igv-flw-button-cancel-color: #c4c4c4; +$igv-flw-button-cancel-hover-color: #7f7f7f; + +// helga dimensions +$igv-flw-container-width: 720px; +$igv-flw-container-height: 400px; + +$igv-flw-button-width: 75px; +$igv-flw-button-height: 28px; + +$igv-flw-input-row-height: 36px; + +$igv-flw-border-radius: 2px; + +@mixin igv-flw-input { + + input { + display: block; + height: 100%; + width: 100%; + + padding-left: 4px; + + color: $igv-flw-dark-grey-color; + font-size: $igv-flw-font-size; + font-family: $igv-flw-font-face; + font-weight: 400; + text-align: left; + + outline: none; + + border-style: solid; + border-width: thin; + border-color: #dee2e6; + border-radius: .25rem; + + background-color: white; + } + +} + +.igv-file-load-widget-container { + + position: relative; + border-color: transparent; + width:100%; + + //padding-bottom: 20px; + + color: $igv-flw-grey-color; + font-family: $igv-flw-font-face; + font-size: $igv-flw-font-size; + font-weight: 200; + + border-style: solid; + border-width: thin; + + background-color: white; + + display: flex; + flex-flow: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + + // header + .igv-file-load-widget-header { + width: 100%; + height: 24px; + + background-color: $igv-flw-light-grey-color; + + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; + + // close button container + div { + height: 24px; + width: 16px; + margin-right: 6px; + + text-align: center; + line-height: 24px; + + color: $igv-flw-dark-grey-color; + } + + div:hover { + cursor: pointer; + } + + } + + // input container + .igv-flw-input-container { + + width: 95%; + + margin-top: 24px; + margin-bottom: 0; + + display: flex; + flex-flow: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + + // input row + .igv-flw-input-row { + + height: $igv-flw-input-row-height; + width: 100%; + + margin-top: 8px; + + padding-top: 4px; + padding-bottom: 4px; + + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + + border-color: white; + border-style: solid; + border-width: thin; + border-radius: calc(2 * #{$igv-flw-border-radius}); + + // label + .igv-flw-input-label { + + //color: rgba(0, 0, 0, 0.76); + color: $igv-flw-modal-button-dark-color; + font-weight: 400; + + //margin-left: 8px; + //width: 128px; + width: 136px; + height: $igv-flw-input-row-height; + + line-height: $igv-flw-input-row-height; + text-align: left; + } + + // url input + @include igv-flw-input; + input { + height: calc(#{$igv-flw-input-row-height} - 12px); + } + + // local file chooser + .igv-flw-file-chooser-container { + display: flex; + flex-flow: row; + justify-content: center; + align-items: center; + + width: 130px; + height: calc(#{$igv-flw-input-row-height} - 8px); + + border-color: $igv-flw-modal-button-dark-color; + border-style: solid; + border-width: thin; + border-radius: calc(2 * #{$igv-flw-border-radius}); + + background-color: white; + + label { + display: block; + margin: unset; + } + + label.igv-flw-label-color { + color: $igv-flw-modal-button-dark-color; + } + + label.igv-flw-label-color-hover { + cursor: pointer; + } + + input.igv-flw-file-chooser-input { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; + } + + } + + .igv-flw-file-chooser-container:hover { + cursor: pointer; + background-color: $igv-flw-modal-button-dark-color; + } + + // button indicating drag/drop capability + .igv-flw-drag-drop-target { + + cursor: default; + + margin-left: 8px; + + width: 120px; + height: calc(#{$igv-flw-input-row-height} - 8px); + + line-height: calc(#{$igv-flw-input-row-height} - 8px); + text-align: center; + + border-color: $igv-flw-grey-color; + border-style: dashed; + border-width: thin; + border-radius: calc(2 * #{$igv-flw-border-radius}); + + //background-color: rgba(173, 255, 47, 0.47); + } + + // name of local file + .igv-flw-local-file-name-container { + + max-width: 400px; + height: $igv-flw-input-row-height; + + color: $igv-flw-dark-grey-color; + line-height: $igv-flw-input-row-height; + text-align: left; + font-weight: 400; + + margin-left: 8px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + //background-color: rgba(128, 128, 128, 0.25); + } + + } + + .igv-flw-input-row-hover-state { + background-color: #efefef; + border-color: $igv-flw-grey-color; + } + + } + + .igv-flw-error-message-container { + + margin-top: 8px; + + width: 95%; + height: 24px; + + padding-left: 8px; + + color: white; + font-size: $igv-flw-font-size; + + background-color: rgba(59, 92, 127, 0.5); + + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + + // message container + div:first-child.igv-flw-error-message { + height: 24px; + width: 600px; + + font-style: italic; + line-height: 24px; + text-align: left; + + //background-color: coral; + } + + // close button container + div:last-child { + height: 24px; + width: 16px; + margin-right: 6px; + + text-align: center; + line-height: 24px; + + color: $igv-flw-dark-grey-color; + } + + div:hover { + cursor: pointer; + } + + } + + // ok | cancel + .igv-file-load-widget-ok-cancel { + + width: 100%; + height: $igv-flw-button-height; + margin-top: 32px; + + color: white; + font-size: $igv-flw-font-size; + + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; + + div { + width: $igv-flw-button-width; + height: $igv-flw-button-height; + + line-height: $igv-flw-button-height; + text-align: center; + + border-color: transparent; + border-style: solid; + border-width: thin; + border-radius: $igv-flw-border-radius; + + margin-right: 16px; + } + + div:first-child { + margin-right: 22px; + background-color: $igv-flw-button-cancel-color; + } + + div:first-child:hover { + cursor: pointer; + background-color: $igv-flw-button-cancel-hover-color; + } + + div:last-child { + background-color: $igv-flw-button-ok-color; + } + + div:last-child:hover { + cursor: pointer; + background-color: $igv-flw-button-ok-hover-color; + } + + } + +} diff --git a/css/_igv-widgets-alert-dialog.scss b/css/_igv-widgets-alert-dialog.scss new file mode 100644 index 00000000..408366f2 --- /dev/null +++ b/css/_igv-widgets-alert-dialog.scss @@ -0,0 +1,143 @@ + +//$igv-alert-dialog-width: 300px; +$igv-alert-dialog-width: 400px; +$igv-alert-dialog-height: 200px; +$igv-alert-dialog-margin: -150px; + +$igv-alert-dialog-header-height: 24px; +$igv-alert-dialog-ok-container-height: 64px; +$igv-alert-dialog-ok-button-height: 30px; +$igv-alert-dialog-body-copy-margin: 16px; + +.igv-widgets-alert-dialog-container { + + box-sizing: content-box; + + position: absolute; + z-index: 2048; + top:50%; + left:50%; + + width:$igv-alert-dialog-width; + height:$igv-alert-dialog-height; + + border-color: $igv-trackgear-grey-color; + border-radius: $igv-trackgear-popover-border-radius; + border-style: solid; + border-width: thin; + + outline: none; + + font-family: $igv-default-font-face; + font-size: 15px; + font-weight: 400; + + background-color: white; + + display: flex; + flex-flow: column; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + + // header + > div:first-child { + + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + + width: 100%; + height: $igv-alert-dialog-header-height; + cursor: move; + + border-top-left-radius: $igv-trackgear-popover-border-radius; + border-top-right-radius: $igv-trackgear-popover-border-radius; + border-bottom-color: $igv-trackgear-grey-color; + border-bottom-style: solid; + border-bottom-width: thin; + + background-color: #eee; + + div:first-child { + // ERROR + padding-left: 8px; + } + + } + + // body container + #igv-widgets-alert-dialog-body { + color: $igv-dark-grey-color; + + width: 100%; + height: calc(100% - #{$igv-alert-dialog-header-height} - #{$igv-alert-dialog-ok-container-height}); + + overflow-y: scroll; + + #igv-widgets-alert-dialog-body-copy { + cursor: pointer; + //user-select: none; + + margin: $igv-alert-dialog-body-copy-margin; + + width: auto; + height: auto; + + //overflow-x: hidden; + //overflow-y: scroll; + + overflow-wrap: break-word; + word-break: break-word; + + background-color: white; + border: unset; + } + } + + // ok - container + > div:last-child { + + width: 100%; + margin-bottom: 10px; + + background-color: white; + + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + + // ok - button + div { + margin: unset; + width: 40px; + height: $igv-alert-dialog-ok-button-height; + + line-height: $igv-alert-dialog-ok-button-height; + text-align: center; + + color: white; + font-family: $igv-default-font-face; + font-size: small; + font-weight: 400; + + border-color: #2B81AF; + border-style: solid; + border-width: thin; + border-radius: 4px; + background-color: #2B81AF; + } + + div:hover { + cursor: pointer; + border-color: #25597f; + background-color: #25597f; + } + + } + +} diff --git a/css/app.css b/css/app.css index 58685e4b..5fee7b2b 100644 --- a/css/app.css +++ b/css/app.css @@ -129,7 +129,323 @@ select#igv-app-track-select-modal-select option { font-size: 1rem; } select#igv-app-track-select-modal-select option[disabled] { - color: rgb(175, 175, 175); + color: #afafaf; +} + +.igv-widgets-alert-dialog-container { + box-sizing: content-box; + position: absolute; + z-index: 2048; + top: 50%; + left: 50%; + width: 400px; + height: 200px; + border-color: #7F7F7F; + border-radius: 4px; + border-style: solid; + border-width: thin; + outline: none; + font-family: "Open Sans", sans-serif; + font-size: 15px; + font-weight: 400; + background-color: white; + display: flex; + flex-flow: column; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; +} +.igv-widgets-alert-dialog-container > div:first-child { + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + width: 100%; + height: 24px; + cursor: move; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-color: #7F7F7F; + border-bottom-style: solid; + border-bottom-width: thin; + background-color: #eee; +} +.igv-widgets-alert-dialog-container > div:first-child div:first-child { + padding-left: 8px; +} +.igv-widgets-alert-dialog-container #igv-widgets-alert-dialog-body { + color: #373737; + width: 100%; + height: calc(100% - 24px - 64px); + overflow-y: scroll; +} +.igv-widgets-alert-dialog-container #igv-widgets-alert-dialog-body #igv-widgets-alert-dialog-body-copy { + cursor: pointer; + margin: 16px; + width: auto; + height: auto; + overflow-wrap: break-word; + word-break: break-word; + background-color: white; + border: unset; +} +.igv-widgets-alert-dialog-container > div:last-child { + width: 100%; + margin-bottom: 10px; + background-color: white; + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: center; + align-items: center; +} +.igv-widgets-alert-dialog-container > div:last-child div { + margin: unset; + width: 40px; + height: 30px; + line-height: 30px; + text-align: center; + color: white; + font-family: "Open Sans", sans-serif; + font-size: small; + font-weight: 400; + border-color: #2B81AF; + border-style: solid; + border-width: thin; + border-radius: 4px; + background-color: #2B81AF; +} +.igv-widgets-alert-dialog-container > div:last-child div:hover { + cursor: pointer; + border-color: #25597f; + background-color: #25597f; +} + +.igv-file-load-widget-container { + position: relative; + border-color: transparent; + width: 100%; + color: #7F7F7F; + font-family: "Open Sans", sans-serif; + font-size: 0.875rem; + font-weight: 200; + border-style: solid; + border-width: thin; + background-color: white; + display: flex; + flex-flow: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; +} +.igv-file-load-widget-container .igv-file-load-widget-header { + width: 100%; + height: 24px; + background-color: #bfbfbf; + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; +} +.igv-file-load-widget-container .igv-file-load-widget-header div { + height: 24px; + width: 16px; + margin-right: 6px; + text-align: center; + line-height: 24px; + color: #373737; +} +.igv-file-load-widget-container .igv-file-load-widget-header div:hover { + cursor: pointer; +} +.igv-file-load-widget-container .igv-flw-input-container { + width: 95%; + margin-top: 24px; + margin-bottom: 0; + display: flex; + flex-flow: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row { + height: 36px; + width: 100%; + margin-top: 8px; + padding-top: 4px; + padding-bottom: 4px; + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + border-color: white; + border-style: solid; + border-width: thin; + border-radius: calc(2 * 2px); +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-input-label { + color: #6c757c; + font-weight: 400; + width: 136px; + height: 36px; + line-height: 36px; + text-align: left; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row input { + display: block; + height: 100%; + width: 100%; + padding-left: 4px; + color: #373737; + font-size: 0.875rem; + font-family: "Open Sans", sans-serif; + font-weight: 400; + text-align: left; + outline: none; + border-style: solid; + border-width: thin; + border-color: #dee2e6; + border-radius: 0.25rem; + background-color: white; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row input { + height: calc(36px - 12px); +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container { + display: flex; + flex-flow: row; + justify-content: center; + align-items: center; + width: 130px; + height: calc(36px - 8px); + border-color: #6c757c; + border-style: solid; + border-width: thin; + border-radius: calc(2 * 2px); + background-color: white; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container label { + display: block; + margin: unset; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container label.igv-flw-label-color { + color: #6c757c; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container label.igv-flw-label-color-hover { + cursor: pointer; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container input.igv-flw-file-chooser-input { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container:hover { + cursor: pointer; + background-color: #6c757c; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-drag-drop-target { + cursor: default; + margin-left: 8px; + width: 120px; + height: calc(36px - 8px); + line-height: calc(36px - 8px); + text-align: center; + border-color: #7F7F7F; + border-style: dashed; + border-width: thin; + border-radius: calc(2 * 2px); +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-local-file-name-container { + max-width: 400px; + height: 36px; + color: #373737; + line-height: 36px; + text-align: left; + font-weight: 400; + margin-left: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row-hover-state { + background-color: #efefef; + border-color: #7F7F7F; +} +.igv-file-load-widget-container .igv-flw-error-message-container { + margin-top: 8px; + width: 95%; + height: 24px; + padding-left: 8px; + color: white; + font-size: 0.875rem; + background-color: rgba(59, 92, 127, 0.5); + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; +} +.igv-file-load-widget-container .igv-flw-error-message-container div:first-child.igv-flw-error-message { + height: 24px; + width: 600px; + font-style: italic; + line-height: 24px; + text-align: left; +} +.igv-file-load-widget-container .igv-flw-error-message-container div:last-child { + height: 24px; + width: 16px; + margin-right: 6px; + text-align: center; + line-height: 24px; + color: #373737; +} +.igv-file-load-widget-container .igv-flw-error-message-container div:hover { + cursor: pointer; +} +.igv-file-load-widget-container .igv-file-load-widget-ok-cancel { + width: 100%; + height: 28px; + margin-top: 32px; + color: white; + font-size: 0.875rem; + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; +} +.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div { + width: 75px; + height: 28px; + line-height: 28px; + text-align: center; + border-color: transparent; + border-style: solid; + border-width: thin; + border-radius: 2px; + margin-right: 16px; +} +.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:first-child { + margin-right: 22px; + background-color: #c4c4c4; +} +.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:first-child:hover { + cursor: pointer; + background-color: #7f7f7f; +} +.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:last-child { + background-color: #5ea4e0; +} +.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:last-child:hover { + cursor: pointer; + background-color: #3b5c7f; } body { diff --git a/css/app.css.map b/css/app.css.map index 14114af8..18167d54 100644 --- a/css/app.css.map +++ b/css/app.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["_panel.scss","_color.scss","_share-modal.scss","_encode.scss","_track-select.scss","app.scss"],"names":[],"mappings":"AAMA;EACE;EACA;EACA;EAEA;EAEA,OAXuB;EAYvB,QAXwB;EAaxB,cCTmB;EDUnB,kBCNe;EDQf;;AAEA;EAEE;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;EAEA;;AAEA;EACE;EACA;EACA;EACA;EACA;EAEA;EACA,QAxCmC;EA0CnC,kBCrCa;EDuCb;EACA;;AAEA;EACE;EACA,OCjDU;EDkDV;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;;;AE5DR;EAEE,OAJmB;;AAMnB;EACE,aAVkB;EAWlB,WAVgB;EAWhB;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE,OAtBmB;EAuBnB;;;AAGF;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EAEA;EAaA;;AAXA;EACE;EACA;;AAIF;EACE;EACA;;;AAMJ;EACE;;;AC3EF;EAEE;EAEA;EACA;EACA;EACA;EACA;;;ACRF;EAEE;;AAEA;EACE;;AAGF;EAGE;;;ACOJ;EACE,aAHkB;;;AAQlB;EACE;;;AAKJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE,YA3DkB;EA4DlB,kBAnEmB;;;AAyEf;EACE;EACA;EACA;;AAEF;EACE;;;AAMR;EACE;EAEA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EACE;;AAEF;EACE;;;AAKJ;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKA;EACE;;AAEA;EACE;;;AAKN;EACE;;;AAGF;EACE;EACA;;;AAKA;EACE;;;AAOF;EACE;;;AAKJ;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAOE;EACE,OApMe;EAqMf;EACA;EACA;;;AAKN;EACE;;;AAKA;EACE,OAnNiB;EAoNjB;EAEA;;AAGF;EAEE;;;AAKJ;EACE;EACA;;;AAGF;EACE,eAjOsB;;;AAoOxB;EACE;EACA;EAEA;EACA,QAzOsB;EA0OtB,aA1OsB;EA4OtB;EACA;EACA;EACA;EACA;;AAEA;EACE,QAnPoB;EAoPpB,aApPoB;EAqPpB;EACA;;AACA;EACE,OA7Pe;EA8Pf;;AAIJ;EACE,QA9PoB;EA+PpB,aA/PoB;EAgQpB;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAMN;EACE;EAEA,QAtRkC;EAwRlC;;;AAIF;EACE,kBAjSmB;;;AAoSrB;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE,OAnTqB;EAoTrB;EACA;;;AAGF;EACE,OAxTmB;EAyTnB,cA1TqB;;;AA6TvB;EACE,kBA9TqB;;;AAiUvB;EACE,kBAjUmB;;;AAoUrB;EACE;;;AAQA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAKJ;EACE;;;AAIF;EACE;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKF;EACE;;;AAKA;EACE;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;EAEE;IACE;IACA;IAEA;IACA,QAzZoB;IA0ZpB,aA1ZoB;IA4ZpB;IACA;IACA;IACA;IACA;;EAEA;IACE,QAnakB;IAoalB,aApakB;IAqalB;IACA;;EACA;IACE,OA7aa;IA8ab;;EAIJ;IACE,QA9akB;IA+alB,aA/akB;IAgblB;IACA;;EAGF;IACE","file":"app.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["_panel.scss","_color.scss","_share-modal.scss","_encode.scss","_track-select.scss","_igv-widgets-alert-dialog.scss","app.scss","_file-load-widget.scss"],"names":[],"mappings":"AAMA;EACE;EACA;EACA;EAEA;EAEA,OAXuB;EAYvB,QAXwB;EAaxB,cCTmB;EDUnB,kBCNe;EDQf;;AAEA;EAEE;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;EAEA;;AAEA;EACE;EACA;EACA;EACA;EACA;EAEA;EACA,QAxCmC;EA0CnC,kBCrCa;EDuCb;EACA;;AAEA;EACE;EACA,OCjDU;EDkDV;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;;;AE5DR;EAEE,OAJmB;;AAMnB;EACE,aAVkB;EAWlB,WAVgB;EAWhB;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE,OAtBmB;EAuBnB;;;AAGF;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EAEA;EAaA;;AAXA;EACE;EACA;;AAIF;EACE;EACA;;;AAMJ;EACE;;;AC3EF;EAEE;EAEA;EACA;EACA;EACA;EACA;;;ACRF;EAEE;;AAEA;EACE;;AAGF;EAGE;;;ACAJ;EAEE;EAEA;EACA;EACA;EACA;EAEA,OAlBuB;EAmBvB,QAlBwB;EAoBxB,cCfyB;EDgBzB,eCfoC;EDgBpC;EACA;EAEA;EAEA,aCvBsB;EDwBtB;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;;AAGA;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA,QA9C6B;EA+C7B;EAEA,wBC9CkC;ED+ClC,yBC/CkC;EDgDlC,qBCjDuB;EDkDvB;EACA;EAEA;;AAEA;EAEE;;AAMJ;EACE,OC9DkB;EDgElB;EACA;EAEA;;AAEA;EACE;EAGA,QA1E8B;EA4E9B;EACA;EAKA;EACA;EAEA;EACA;;AAKJ;EAEE;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;;AAGA;EACE;EACA;EACA,QA7G8B;EA+G9B,aA/G8B;EAgH9B;EAEA;EACA,aCpHkB;EDqHlB;EACA;EAEA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AEnFN;EAEE;EACA;EACA;EAIA,OA3DmB;EA4DnB,aA/DkB;EAgElB,WA/DkB;EAgElB;EAEA;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;;AAGA;EACE;EACA;EAEA,kBAjFuB;EAmFvB;EACA;EACA;EACA;EACA;;AAGA;EACE;EACA;EACA;EAEA;EACA;EAEA,OAhGoB;;AAmGtB;EACE;;AAMJ;EAEE;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;;AAGA;EAEE,QAvGqB;EAwGrB;EAEA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;;AAGA;EAGE,OA5I0B;EA6I1B;EAIA;EACA,QApImB;EAsInB,aAtImB;EAuInB;;AAjIN;EACE;EACA;EACA;EAEA;EAEA,OAhCsB;EAiCtB,WApCgB;EAqChB,aAtCgB;EAuChB;EACA;EAEA;EAEA;EACA;EACA;EACA;EAEA;;AAkHE;EACE;;AAIF;EACE;EACA;EACA;EACA;EAEA;EACA;EAEA,cAxK0B;EAyK1B;EACA;EACA;EAEA;;AAEA;EACE;EACA;;AAGF;EACE,OArLwB;;AAwL1B;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACE;EACA,kBAzM0B;;AA6M5B;EAEE;EAEA;EAEA;EACA;EAEA;EACA;EAEA,cA/Na;EAgOb;EACA;EACA;;AAMF;EAEE;EACA,QAvNmB;EAyNnB,OA5OkB;EA6OlB,aA1NmB;EA2NnB;EACA;EAEA;EAEA;EACA;EACA;;AAOJ;EACE;EACA,cA/Pe;;AAoQnB;EAEE;EAEA;EACA;EAEA;EAEA;EACA,WAhRgB;EAkRhB;EAEA;EACA;EACA;EACA;EACA;;AAGA;EACE;EACA;EAEA;EACA;EACA;;AAMF;EACE;EACA;EACA;EAEA;EACA;EAEA,OA5SoB;;AA+StB;EACE;;AAMJ;EAEE;EACA,QAxSoB;EAySpB;EAEA;EACA,WAhUgB;EAkUhB;EACA;EACA;EACA;EACA;;AAEA;EACE,OAtTiB;EAuTjB,QAtTkB;EAwTlB,aAxTkB;EAyTlB;EAEA;EACA;EACA;EACA,eA1TkB;EA4TlB;;AAGF;EACE;EACA,kBA7UwB;;AAgV1B;EACE;EACA,kBAjV8B;;AAoVhC;EACE,kBAxVoB;;AA2VtB;EACE;EACA,kBA5V0B;;;ADchC;EACE,aAPkB;;;AAYlB;EACE;;;AAKJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE,YA/DkB;EAgElB,kBAvEmB;;;AA6Ef;EACE;EACA;EACA;;AAEF;EACE;;;AAMR;EACE;EAEA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EACE;;AAEF;EACE;;;AAKJ;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKA;EACE;;AAEA;EACE;;;AAKN;EACE;;;AAGF;EACE;EACA;;;AAKA;EACE;;;AAOF;EACE;;;AAKJ;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAOE;EACE,OAxMe;EAyMf;EACA;EACA;;;AAKN;EACE;;;AAKA;EACE,OAvNiB;EAwNjB;EAEA;;AAGF;EAEE;;;AAKJ;EACE;EACA;;;AAGF;EACE,eArOsB;;;AAwOxB;EACE;EACA;EAEA;EACA,QA7OsB;EA8OtB,aA9OsB;EAgPtB;EACA;EACA;EACA;EACA;;AAEA;EACE,QAvPoB;EAwPpB,aAxPoB;EAyPpB;EACA;;AACA;EACE,OAjQe;EAkQf;;AAIJ;EACE,QAlQoB;EAmQpB,aAnQoB;EAoQpB;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAMN;EACE;EAEA,QA1RkC;EA4RlC;;;AAIF;EACE,kBArSmB;;;AAwSrB;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE,OAvTqB;EAwTrB;EACA;;;AAGF;EACE,OA5TmB;EA6TnB,cA9TqB;;;AAiUvB;EACE,kBAlUqB;;;AAqUvB;EACE,kBArUmB;;;AAwUrB;EACE;;;AAQA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAKJ;EACE;;;AAIF;EACE;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKF;EACE;;;AAKA;EACE;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;EAEE;IACE;IACA;IAEA;IACA,QA7ZoB;IA8ZpB,aA9ZoB;IAgapB;IACA;IACA;IACA;IACA;;EAEA;IACE,QAvakB;IAwalB,aAxakB;IAyalB;IACA;;EACA;IACE,OAjba;IAkbb;;EAIJ;IACE,QAlbkB;IAmblB,aAnbkB;IAoblB;IACA;;EAGF;IACE","file":"app.css"} \ No newline at end of file diff --git a/css/app.scss b/css/app.scss index e11eb839..939ca494 100644 --- a/css/app.scss +++ b/css/app.scss @@ -5,6 +5,10 @@ @import "track-select"; // +$igv-default-font-face: 'Open Sans', sans-serif; +$igv-trackgear-grey-color: #7F7F7F; +$igv-trackgear-popover-border-radius: 4px; +$igv-dark-grey-color: #373737; $igv-app-light-color: #f7f7f7; $igv-app-medium-color: #a6a6a6; $igv-app-dark-color: #5f5f5f; @@ -16,6 +20,10 @@ $igv-app-footer-height: 48px; $igv-app-logo-container-width: 20%; $igv-header-height: 48px; +@import "igv-widgets-alert-dialog"; +@import "file-load-widget"; + + body { padding-top: $igv-header-height; } diff --git a/js/app.js b/js/app.js index 3c6d4b78..a601b2c6 100644 --- a/js/app.js +++ b/js/app.js @@ -24,22 +24,15 @@ import igv from '../node_modules/igv/dist/igv.esm.min.js' import * as GoogleAuth from '../node_modules/google-utils/src/googleAuth.js' import * as GooglePicker from '../node_modules/google-utils/src/googleFilePicker.js' -import {makeDraggable} from "./draggable.js" -import { - AlertSingleton, - createSessionWidgets, - createTrackWidgetsWithTrackRegistry, - dropboxButtonImageBase64, - dropboxDropdownItem, - GenomeFileLoad, - googleDriveButtonImageBase64, - googleDriveDropdownItem, - getPathsWithTrackRegistryFile, - updateTrackMenusWithTrackConfigurations, - FileLoadManager, - FileLoadWidget, - Utils -} from '../node_modules/igv-widgets/dist/igv-widgets.js' +import makeDraggable from "./widgets/utils/draggable.js" +import AlertSingleton from "./widgets/alertSingleton.js" +import {createSessionWidgets} from "./widgets/sessionWidgets.js" +import {updateTrackMenusWithTrackConfigurations, createTrackWidgetsWithTrackRegistry, getPathsWithTrackRegistryFile} from "./widgets/trackWidgets.js" +import {dropboxDropdownItem, dropboxButtonImageBase64, googleDriveButtonImageBase64, googleDriveDropdownItem} from "./widgets/markupFactory.js" +import GenomeFileLoad from "./widgets/genomeFileLoad.js" +import FileLoadManager from "./widgets/fileLoadManager.js" +import FileLoadWidget from "./widgets/fileLoadWidget.js" +import * as Utils from './widgets/utils.js' import Globals from "./globals.js" import {createGenomeWidgets, initializeGenomeWidgets, loadGenome} from './genomeWidgets.js' import {createShareWidgets, shareWidgetConfigurator} from './shareWidgets.js' diff --git a/js/genomeWidgets.js b/js/genomeWidgets.js index 07dba523..1b53d26c 100644 --- a/js/genomeWidgets.js +++ b/js/genomeWidgets.js @@ -24,13 +24,11 @@ * THE SOFTWARE. */ -import { - AlertSingleton, - createURLModal, - FileLoadManager, - FileLoadWidget, - Utils -} from '../node_modules/igv-widgets/dist/igv-widgets.js' +import AlertSingleton from "./widgets/alertSingleton.js" +import {createURLModal} from "./widgets/urlModal.js" +import FileLoadManager from "./widgets/fileLoadManager.js" +import FileLoadWidget from "./widgets/fileLoadWidget.js" +import * as Utils from './widgets/utils.js' import Globals from "./globals.js" const MAX_CUSTOM_GENOMES = 10 diff --git a/js/shareHelper.js b/js/shareHelper.js index 3291694c..0be8e685 100644 --- a/js/shareHelper.js +++ b/js/shareHelper.js @@ -20,7 +20,7 @@ * THE SOFTWARE. * */ -import {AlertSingleton} from '../node_modules/igv-widgets/dist/igv-widgets.js' +import AlertSingleton from "./widgets/alertSingleton.js" import {bitlyShortener, googleShortener, tinyURLShortener} from "./urlShortener.js"; import Globals from "./globals.js"; diff --git a/js/shareWidgets.js b/js/shareWidgets.js index f4f6c3e8..0e91fe1e 100644 --- a/js/shareWidgets.js +++ b/js/shareWidgets.js @@ -22,7 +22,8 @@ */ import igv from '../node_modules/igv/dist/igv.esm.min.js' -import {AlertSingleton, QRCode} from '../node_modules/igv-widgets/dist/igv-widgets.js' +import AlertSingleton from './widgets/alertSingleton.js' +import {QRCode} from './widgets/qrcode.js' import {setURLShortener, shortSessionURL} from './shareHelper.js' function createShareWidgets({browser, container, modal, share_input, copy_link_button, email_button, qrcode_button, qrcode_image, embed_container, embed_button, embedTarget}) { diff --git a/js/widgets/alertDialog.js b/js/widgets/alertDialog.js new file mode 100644 index 00000000..06626764 --- /dev/null +++ b/js/widgets/alertDialog.js @@ -0,0 +1,92 @@ +import * as DOMUtils from "./utils/dom-utils.js" +import makeDraggable from "./utils/draggable.js" + +const httpMessages = + { + "401": "Access unauthorized", + "403": "Access forbidden", + "404": "Not found" + } + + +class AlertDialog { + constructor(parent) { + + // container + this.container = DOMUtils.div({class: "igv-widgets-alert-dialog-container"}) + parent.appendChild(this.container) + this.container.setAttribute('tabIndex', '-1') + + // header + const header = DOMUtils.div() + this.container.appendChild(header) + + this.errorHeadline = DOMUtils.div() + header.appendChild(this.errorHeadline) + this.errorHeadline.textContent = '' + + // body container + let bodyContainer = DOMUtils.div({id: 'igv-widgets-alert-dialog-body'}) + this.container.appendChild(bodyContainer) + + // body copy + this.body = DOMUtils.div({id: 'igv-widgets-alert-dialog-body-copy'}) + bodyContainer.appendChild(this.body) + + // ok container + let ok_container = DOMUtils.div() + this.container.appendChild(ok_container) + + // ok + this.ok = DOMUtils.div() + ok_container.appendChild(this.ok) + this.ok.textContent = 'OK' + + const okHandler = () => { + + if (typeof this.callback === 'function') { + this.callback("OK") + this.callback = undefined + } + this.body.innerHTML = '' + DOMUtils.hide(this.container) + } + + this.ok.addEventListener('click', event => { + + event.stopPropagation() + + okHandler() + }) + + this.container.addEventListener('keypress', event => { + + event.stopPropagation() + + if ('Enter' === event.key) { + okHandler() + } + }) + + makeDraggable(this.container, header) + + DOMUtils.hide(this.container) + } + + present(alert, callback) { + + this.errorHeadline.textContent = alert.message ? 'ERROR' : '' + let string = alert.message || alert + + if (httpMessages.hasOwnProperty(string)) { + string = httpMessages[string] + } + + this.body.innerHTML = string + this.callback = callback + DOMUtils.show(this.container, "flex") + this.container.focus() + } +} + +export default AlertDialog diff --git a/js/widgets/alertSingleton.js b/js/widgets/alertSingleton.js new file mode 100644 index 00000000..23dd2426 --- /dev/null +++ b/js/widgets/alertSingleton.js @@ -0,0 +1,21 @@ +import AlertDialog from './alertDialog.js' + +class AlertSingleton { + constructor(root) { + + if (root) { + this.alertDialog = undefined + } + } + + init(root) { + this.alertDialog = new AlertDialog(root) + } + + present(alert, callback) { + this.alertDialog.present(alert, callback) + } + +} + +export default new AlertSingleton() diff --git a/js/widgets/encodeTrackDatasourceConfigurator.js b/js/widgets/encodeTrackDatasourceConfigurator.js new file mode 100644 index 00000000..a49e501c --- /dev/null +++ b/js/widgets/encodeTrackDatasourceConfigurator.js @@ -0,0 +1,190 @@ +/** + * Factory function to create a configuration object for the EncodeTrackDatasource given a genomicId and type + * @param genomeId + * @param type - 'signals' | 'other + * @returns {{genomeId: *, selectionHandler: (function(*): *|Uint8Array|BigInt64Array|{color, name, url}[]|Float64Array|Int8Array|Float32Array|Int32Array|Uint32Array|Uint8ClampedArray|BigUint64Array|Int16Array|Uint16Array), hiddenColumns: [string, string, string], addIndexColumn: boolean, parser: undefined, isJSON: boolean, urlPrefix: string, columns: string[], dataSetPath: undefined, titles: {AssayType: string, BioRep: string, OutputType: string, TechRep: string}, suffix: *, dataSetPathPrefix: string}} + */ +function encodeTrackDatasourceConfigurator(genomeId, type) { + + const root = 'https://s3.amazonaws.com/igv.org.app/encode/' + let url + + switch (type) { + case 'signals-chip': + url = `${root}${canonicalId(genomeId)}.signals.chip.txt` + break + case 'signals-other': + url = `${root}${canonicalId(genomeId)}.signals.other.txt` + break + case 'other': + url = `${root}${canonicalId(genomeId)}.other.txt` + break + + } + + return { + isJSON: false, + url, + sort: encodeSort, + columns: + [ + //'ID', // hide + //'Assembly', // hide + 'Biosample', + 'AssayType', + 'Target', + 'BioRep', + 'TechRep', + 'OutputType', + 'Format', + 'Lab', + //'HREF', // hide + 'Accession', + 'Experiment' + ], + columnDefs: + { + AssayType: {title: 'Assay Type'}, + OutputType: {title: 'Output Type'}, + BioRep: {title: 'Bio Rep'}, + TechRep: {title: 'Tech Rep'} + }, + + rowHandler: row => { + const name = constructName(row) + const url = `https://www.encodeproject.org${row['HREF']}` + const color = colorForTarget(row['Target']) + return {name, url, color} + } + + } +} + + +function supportsGenome(genomeId) { + const knownGenomes = new Set(["ce10", "ce11", "dm3", "dm6", "GRCh38", "hg19", "mm9", "mm10"]) + const id = canonicalId(genomeId) + return knownGenomes.has(id) +} + + +function canonicalId(genomeId) { + + switch (genomeId) { + case "hg38": + return "GRCh38" + case "CRCh37": + return "hg19" + case "GRCm38": + return "mm10" + case "NCBI37": + return "mm9" + case "WBcel235": + return "ce11" + case "WS220": + return "ce10" + default: + return genomeId + } +} + +function constructName(record) { + + let name = record["Biosample"] || "" + + if (record["Target"]) { + name += " " + record["Target"] + } + if (record["AssayType"].toLowerCase() !== "chip-seq") { + name += " " + record["AssayType"] + } + + + return name + +} + +// Longer form of constructName, not currently used +function _constructName(record) { + let name = record["Cell Type"] || "" + + if (record["Target"]) { + name += " " + record["Target"] + } + if (record["AssayType"] && record["AssayType"].toLowerCase() !== "chip-seq") { + name += " " + record["AssayType"] + } + if (record["BioRep"]) { + name += " " + record["BioRep"] + } + if (record["TechRep"]) { + name += (record["BioRep"] ? ":" : " 0:") + record["TechRep"] + } + if (record["OutputType"]) { + name += " " + record["OutputType"] + } + if (record["Accession"]) { + name += " " + record["Accession"] + } + return name +} + + +function encodeSort(a, b) { + var aa1, + aa2, + cc1, + cc2, + tt1, + tt2 + + aa1 = a['Assay Type'] + aa2 = b['Assay Type'] + cc1 = a['Biosample'] + cc2 = b['Biosample'] + tt1 = a['Target'] + tt2 = b['Target'] + + if (aa1 === aa2) { + if (cc1 === cc2) { + if (tt1 === tt2) { + return 0 + } else if (tt1 < tt2) { + return -1 + } else { + return 1 + } + } else if (cc1 < cc2) { + return -1 + } else { + return 1 + } + } else { + if (aa1 < aa2) { + return -1 + } else { + return 1 + } + } +} + +function colorForTarget(target) { + + const t = target.toLowerCase() + if (t.startsWith("h3k4")) { + return "rgb(0,150,0)" + } else if (t.startsWith("h3k27")) { + return "rgb(200,0,0)" + } else if (t.startsWith("h3k36")) { + return "rgb(0,0,150)" + } else if (t.startsWith("h3k9")) { + return "rgb(100,0,0)" + } else if (t === "ctcf") { + return "black" + } else { + return undefined + } +} + + +export {encodeTrackDatasourceConfigurator, supportsGenome} diff --git a/js/widgets/eventBus.js b/js/widgets/eventBus.js new file mode 100644 index 00000000..b0723793 --- /dev/null +++ b/js/widgets/eventBus.js @@ -0,0 +1,43 @@ +let subscribers = {} + +class EventBus { + constructor() { + + } + + subscribe(eventType, object) { + + let subscriberList = subscribers[eventType] + if (undefined === subscriberList) { + subscriberList = [] + subscribers[eventType] = subscriberList + } + subscriberList.push(object) + } + + post(event) { + + const subscriberList = subscribers[event.type] + if (subscriberList) { + + for (let subscriber of subscriberList) { + + if ("function" === typeof subscriber.receiveEvent) { + subscriber.receiveEvent(event) + } else if ("function" === typeof subscriber) { + subscriber(event) + } + } + } + } + + static createEvent(type, data, propogate) { + return {type: type, data: data || {}, propogate: propogate !== undefined ? propogate : true} + } + +} + +// Global event bus +EventBus.globalBus = new EventBus() + +export default EventBus diff --git a/js/widgets/fileLoad.js b/js/widgets/fileLoad.js new file mode 100644 index 00000000..74a4b5f0 --- /dev/null +++ b/js/widgets/fileLoad.js @@ -0,0 +1,88 @@ +import AlertSingleton from './alertSingleton.js' +import * as DOMUtils from "./utils/dom-utils.js" +import {GooglePicker} from "../../node_modules/igv-utils/src/index.js" + +class FileLoad { + + constructor({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton}) { + + localFileInput.addEventListener('change', async () => { + + if (true === FileLoad.isValidLocalFileInput(localFileInput)) { + + try { + await this.loadPaths(Array.from(localFileInput.files)) + } catch (e) { + console.error(e) + AlertSingleton.present(e) + } + localFileInput.value = '' + } + + }) + + if (dropboxButton) dropboxButton.addEventListener('click', async () => { + + const result = await initializeDropbox() + + if (true === result) { + + const config = + { + success: async dbFiles => { + try { + await this.loadPaths(dbFiles.map(dbFile => dbFile.link)) + } catch (e) { + console.error(e) + AlertSingleton.present(e) + } + }, + cancel: () => { + }, + linkType: 'preview', + multiselect: true, + folderselect: false, + } + + Dropbox.choose(config) + + } else { + AlertSingleton.present('Cannot connect to Dropbox') + } + + }) + + + if (false === googleEnabled) { + DOMUtils.hide(googleDriveButton.parentElement) + } + + if (true === googleEnabled && googleDriveButton) { + + googleDriveButton.addEventListener('click', () => { + GooglePicker.createDropdownButtonPicker(true, async responses => { + + try { + await this.loadPaths(responses.map(({url}) => url)) + } catch (e) { + console.error(e) + AlertSingleton.present(e) + } + }) + }) + + } + + } + + async loadPaths(paths) { + //console.log('FileLoad: loadPaths(...)'); + } + + static isValidLocalFileInput(input) { + return (input.files && input.files.length > 0) + } + +} + +export default FileLoad diff --git a/js/widgets/fileLoadManager.js b/js/widgets/fileLoadManager.js new file mode 100644 index 00000000..ab1c6049 --- /dev/null +++ b/js/widgets/fileLoadManager.js @@ -0,0 +1,62 @@ +import {FileUtils} from "../../node_modules/igv-utils/src/index.js" + +class FileLoadManager { + + constructor() { + this.dictionary = {} + } + + inputHandler(path, isIndexFile) { + this.ingestPath(path, isIndexFile) + } + + ingestPath(path, isIndexFile) { + let key = true === isIndexFile ? 'index' : 'data' + + this.dictionary[key] = path.trim() + } + + didDragDrop(dataTransfer) { + var files + + files = dataTransfer.files + + return (files && files.length > 0) + } + + dragDropHandler(dataTransfer, isIndexFile) { + var url, + files, + isValid + + url = dataTransfer.getData('text/uri-list') + files = dataTransfer.files + + if (files && files.length > 0) { + this.ingestPath(files[0], isIndexFile) + } else if (url && '' !== url) { + this.ingestPath(url, isIndexFile) + } + + } + + indexName() { + return itemName(this.dictionary.index) + } + + dataName() { + return itemName(this.dictionary.data) + } + + reset() { + this.dictionary = {} + } + +} + +function itemName(item) { + return FileUtils.isFilePath(item) ? item.name : item +} + +export default FileLoadManager + diff --git a/js/widgets/fileLoadWidget.js b/js/widgets/fileLoadWidget.js new file mode 100644 index 00000000..02d1c219 --- /dev/null +++ b/js/widgets/fileLoadWidget.js @@ -0,0 +1,224 @@ +import * as DOMUtils from "./utils/dom-utils.js" +import * as UIUtils from "./utils/ui-utils.js" + +class FileLoadWidget { + + constructor({widgetParent, dataTitle, indexTitle, mode, fileLoadManager, dataOnly, doURL}) { + + dataTitle = dataTitle || 'Data' + + indexTitle = indexTitle || 'Index' + + this.fileLoadManager = fileLoadManager + + dataOnly = dataOnly || false + + // TODO: Remove? + doURL = doURL || false + + // file load widget + this.container = DOMUtils.div({class: 'igv-file-load-widget-container'}) + widgetParent.appendChild(this.container) + + let config + if ('localFile' === mode) { + // local data/index + config = + { + parent: this.container, + doURL: false, + dataTitle: dataTitle + ' file', + indexTitle: indexTitle + ' file', + dataOnly + } + } else { + + // url data/index + config = + { + parent: this.container, + doURL: true, + dataTitle: dataTitle + ' URL', + indexTitle: indexTitle + ' URL', + dataOnly + } + } + + this.createInputContainer(config) + + // error message container + this.error_message = DOMUtils.div({class: 'igv-flw-error-message-container'}) + this.container.appendChild(this.error_message) + + // error message + this.error_message.appendChild(DOMUtils.div({class: 'igv-flw-error-message'})) + + // error dismiss button + UIUtils.attachDialogCloseHandlerWithParent(this.error_message, () => { + this.dismissErrorMessage() + }) + + this.dismissErrorMessage() + + } + + retrievePaths() { + + this.fileLoadManager.ingestPath(this.inputData.value, false) + if (this.inputIndex) { + this.fileLoadManager.ingestPath(this.inputIndex.value, true) + } + + let paths = [] + if (this.fileLoadManager.dictionary) { + + if (this.fileLoadManager.dictionary.data) { + paths.push(this.fileLoadManager.dictionary.data) + } + if (this.fileLoadManager.dictionary.index) { + paths.push(this.fileLoadManager.dictionary.index) + } + } + + // clear input elements + this.container.querySelectorAll('.igv-flw-input-row').forEach(div => { + div.querySelector('input').value = '' + }) + + return paths + + } + + presentErrorMessage(message) { + this.error_message.querySelector('.igv-flw-error-message').textContent = message + DOMUtils.show(this.error_message) + } + + dismissErrorMessage() { + DOMUtils.hide(this.error_message) + this.error_message.querySelector('.igv-flw-error-message').textContent = '' + } + + present() { + DOMUtils.show(this.container) + } + + dismiss() { + + this.dismissErrorMessage() + + // const e = this.container.querySelector('.igv-flw-local-file-name-container'); + // if (e) { + // DOMUtils.hide(e); + // } + + // clear input elements + this.container.querySelectorAll('.igv-flw-input-row').forEach(div => { + div.querySelector('input').value = '' + }) + + this.fileLoadManager.reset() + + } + + createInputContainer({parent, doURL, dataTitle, indexTitle, dataOnly}) { + + // container + const container = DOMUtils.div({class: 'igv-flw-input-container'}) + parent.appendChild(container) + + // data + const input_data_row = DOMUtils.div({class: 'igv-flw-input-row'}) + container.appendChild(input_data_row) + + let label + + // label + label = DOMUtils.div({class: 'igv-flw-input-label'}) + input_data_row.appendChild(label) + label.textContent = dataTitle + + if (true === doURL) { + this.createURLContainer(input_data_row, 'igv-flw-data-url', false) + } else { + this.createLocalFileContainer(input_data_row, 'igv-flw-local-data-file', false) + } + + if (true === dataOnly) { + return + } + + // index + const input_index_row = DOMUtils.div({class: 'igv-flw-input-row'}) + container.appendChild(input_index_row) + + // label + label = DOMUtils.div({class: 'igv-flw-input-label'}) + input_index_row.appendChild(label) + label.textContent = indexTitle + + if (true === doURL) { + this.createURLContainer(input_index_row, 'igv-flw-index-url', true) + } else { + this.createLocalFileContainer(input_index_row, 'igv-flw-local-index-file', true) + } + + } + + createURLContainer(parent, id, isIndexFile) { + + const input = DOMUtils.create('input') + input.setAttribute('type', 'text') + // input.setAttribute('placeholder', (true === isIndexFile ? 'Enter index URL' : 'Enter data URL')); + parent.appendChild(input) + + if (isIndexFile) { + this.inputIndex = input + } else { + this.inputData = input + } + + } + + createLocalFileContainer(parent, id, isIndexFile) { + + const file_chooser_container = DOMUtils.div({class: 'igv-flw-file-chooser-container'}) + parent.appendChild(file_chooser_container) + + const str = `${id}${DOMUtils.guid()}` + + const label = DOMUtils.create('label') + label.setAttribute('for', str) + + file_chooser_container.appendChild(label) + label.textContent = 'Choose file' + + const input = DOMUtils.create('input', {class: 'igv-flw-file-chooser-input'}) + input.setAttribute('id', str) + input.setAttribute('name', str) + input.setAttribute('type', 'file') + file_chooser_container.appendChild(input) + + const file_name = DOMUtils.div({class: 'igv-flw-local-file-name-container'}) + parent.appendChild(file_name) + + DOMUtils.hide(file_name) + + input.addEventListener('change', e => { + + this.dismissErrorMessage() + + const file = e.target.files[0] + this.fileLoadManager.inputHandler(file, isIndexFile) + + const {name} = file + file_name.textContent = name + file_name.setAttribute('title', name) + DOMUtils.show(file_name) + }) + + } + +} + +export default FileLoadWidget diff --git a/js/widgets/genericSelectModal.js b/js/widgets/genericSelectModal.js new file mode 100644 index 00000000..9f5b8f4c --- /dev/null +++ b/js/widgets/genericSelectModal.js @@ -0,0 +1,35 @@ +function createGenericSelectModal(id, select_id) { + + return `` + +} +export {createGenericSelectModal} diff --git a/js/widgets/genomeFileLoad.js b/js/widgets/genomeFileLoad.js new file mode 100644 index 00000000..9058f6e6 --- /dev/null +++ b/js/widgets/genomeFileLoad.js @@ -0,0 +1,75 @@ +import {FileUtils, igvxhr, StringUtils} from "../../node_modules/igv-utils/src/index.js" +import FileLoad from "./fileLoad.js" +import MultipleTrackFileLoad from './multipleTrackFileLoad.js' + +class GenomeFileLoad extends FileLoad { + + constructor({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton, loadHandler}) { + super({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton}) + this.loadHandler = loadHandler + } + + async loadPaths(paths) { + + const status = await GenomeFileLoad.isGZip(paths) + + if (true === status) { + throw new Error('Genome did not load - gzip files are not supported') + } else { + + let configuration = undefined + + const jsonFiles = paths.filter(path => 'json' === FileUtils.getExtension(path)) + const hubFiles = paths.filter(path => StringUtils.isString(path) && path.endsWith("/hub.txt")) + + // If one of the paths is .json, unpack and send to loader + // TODO -- what if multiple json files are selected? This is surely an error + if (jsonFiles.length >= 1) { + configuration = await igvxhr.loadJson(jsonFiles[0]) + } else if (hubFiles.length >= 1) { + configuration = {url: hubFiles[0]} + } else if (2 === paths.length) { + const [_0, _1] = await GenomeFileLoad.getExtension(paths) + if ('fai' === _0) { + configuration = {fastaURL: paths[1], indexURL: paths[0]} + } else if ('fai' === _1) { + configuration = {fastaURL: paths[0], indexURL: paths[1]} + } + } + + if (undefined === configuration) { + throw new Error('Genome requires either a single JSON file or a FASTA file & index file') + } else { + this.loadHandler(configuration) + } + + + } + + } + + static async isGZip(paths) { + + for (let path of paths) { + const filename = await MultipleTrackFileLoad.getFilename(path) + if (true === filename.endsWith('.gz')) { + return true + } + } + + return false + } + + static async getExtension(paths) { + + const a = await MultipleTrackFileLoad.getFilename(paths[0]) + const b = await MultipleTrackFileLoad.getFilename(paths[1]) + + return [a, b].map(name => FileUtils.getExtension(name)) + + } + + +} + +export default GenomeFileLoad diff --git a/js/widgets/markupFactory.js b/js/widgets/markupFactory.js new file mode 100644 index 00000000..4a4a8431 --- /dev/null +++ b/js/widgets/markupFactory.js @@ -0,0 +1,44 @@ +const dropboxButtonImageLiteral = + ` + Shape + Created with Sketch. + + + + + + + ` + +const googleDriveImageLiteral = + `` + +const dropboxButtonImageBase64 = () => window.btoa(dropboxButtonImageLiteral) + +const googleDriveButtonImageBase64 = () => window.btoa(googleDriveImageLiteral) + +const dropboxDropdownItem = id => { + + return `` +} + +const googleDriveDropdownItem = id => { + + return `` +} + +export {dropboxButtonImageBase64, googleDriveButtonImageBase64, dropboxDropdownItem, googleDriveDropdownItem} diff --git a/js/widgets/multipleTrackFileLoad.js b/js/widgets/multipleTrackFileLoad.js new file mode 100644 index 00000000..b250e4a6 --- /dev/null +++ b/js/widgets/multipleTrackFileLoad.js @@ -0,0 +1,181 @@ +import AlertSingleton from './alertSingleton.js' +import {FileUtils, URIUtils, GoogleUtils, GoogleDrive, GooglePicker} from "../../node_modules/igv-utils/src/index.js" + +class MultipleTrackFileLoad { + + constructor({ + $localFileInput, + initializeDropbox, + $dropboxButton, + $googleDriveButton, + fileLoadHandler, + multipleFileSelection + }) { + + this.fileLoadHandler = fileLoadHandler + + const localFileInput = $localFileInput.get(0) + const dropboxButton = $dropboxButton ? $dropboxButton.get(0) : undefined + const googleDriveButton = $googleDriveButton ? $googleDriveButton.get(0) : undefined + + localFileInput.addEventListener('change', async () => { + + if (true === MultipleTrackFileLoad.isValidLocalFileInput(localFileInput)) { + const {files} = localFileInput + const paths = Array.from(files) + localFileInput.value = '' + await this.loadPaths(paths) + } + + }) + + if (dropboxButton) dropboxButton.addEventListener('click', async () => { + + const result = await initializeDropbox() + + if (true === result) { + + const obj = + { + success: dbFiles => this.loadPaths(dbFiles.map(({link}) => link)), + cancel: () => { + }, + linkType: "preview", + multiselect: multipleFileSelection, + folderselect: false, + } + + Dropbox.choose(obj) + + } else { + AlertSingleton.present('Cannot connect to Dropbox') + } + }) + + + if (googleDriveButton) { + + googleDriveButton.addEventListener('click', () => { + GooglePicker.createDropdownButtonPicker(multipleFileSelection, + async responses => await this.loadPaths(responses.map(({ + name, + url + }) => url))) + }) + + } + + } + + async loadPaths(paths) { + await ingestPaths({paths, fileLoadHandler: this.fileLoadHandler}) + } + + static isValidLocalFileInput(input) { + return (input.files && input.files.length > 0) + } + + static async getFilename(path) { + + if (path instanceof File) { + return path.name + } else if (GoogleUtils.isGoogleDriveURL(path)) { + const info = await GoogleDrive.getDriveFileInfo(path) + return info.name || info.originalFileName + } else { + const result = URIUtils.parseUri(path) + return result.file + } + + } + + static isGoogleDrivePath(path) { + return path instanceof File ? false : GoogleUtils.isGoogleDriveURL(path) + } + +} + +async function ingestPaths({paths, fileLoadHandler}) { + try { + // Search for index files (.bai, .csi, .tbi, .idx) + const indexLUT = new Map() + + const dataPaths = [] + for (let path of paths) { + + const name = await MultipleTrackFileLoad.getFilename(path) + const extension = FileUtils.getExtension(name) + + if (indexExtensions.has(extension)) { + + // key is the data file name + const key = createIndexLUTKey(name, extension) + indexLUT.set(key, { + indexURL: path, + indexFilename: MultipleTrackFileLoad.isGoogleDrivePath(path) ? name : undefined + }) + } else { + dataPaths.push(path) + } + + } + + const configurations = [] + + for (let dataPath of dataPaths) { + + const filename = await MultipleTrackFileLoad.getFilename(dataPath) + + if (indexLUT.has(filename)) { + + const {indexURL, indexFilename} = indexLUT.get(filename) + configurations.push({ + url: dataPath, + filename, + indexURL, + indexFilename, + name: filename, + _derivedName: true + }) + + } else if (requireIndex.has(FileUtils.getExtension(filename))) { + throw new Error(`Unable to load track file ${filename} - you must select both ${filename} and its corresponding index file`) + } else { + configurations.push({url: dataPath, filename, name: filename, _derivedName: true}) + } + + } + + if (configurations) { + fileLoadHandler(configurations) + } + + } catch (e) { + console.error(e) + AlertSingleton.present(e.message) + } +} + +const indexExtensions = new Set(['bai', 'csi', 'tbi', 'idx', 'crai', 'fai']) + +const requireIndex = new Set(['bam', 'cram', 'fa', 'fasta']) + +const createIndexLUTKey = (name, extension) => { + + let key = name.substring(0, name.length - (extension.length + 1)) + + // bam and cram files (.bai, .crai) have 2 conventions: + // .bam.bai + // .bai - we will support this one + + if ('bai' === extension && !key.endsWith('bam')) { + return `${key}.bam` + } else if ('crai' === extension && !key.endsWith('cram')) { + return `${key}.cram` + } else { + return key + } + +} + +export default MultipleTrackFileLoad diff --git a/js/widgets/qrcode.js b/js/widgets/qrcode.js new file mode 100755 index 00000000..e387e987 --- /dev/null +++ b/js/widgets/qrcode.js @@ -0,0 +1,1000 @@ +/** + * @fileoverview + * - Using the 'QRCode for Javascript library' + * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. + * - this library has no dependencies. + * + * @author davidshimjs + * @see http://www.d-project.com/ + * @see http://jeromeetienne.github.com/jquery-qrcode/ + */ + + +//--------------------------------------------------------------------- +// QRCode for JavaScript +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word "QR Code" is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- +function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE + this.data = data + this.parsedData = [] + + // Added to support UTF-8 Characters + for (var i = 0, l = this.data.length; i < l; i++) { + var byteArray = [] + var code = this.data.charCodeAt(i) + + if (code > 0x10000) { + byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18) + byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12) + byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6) + byteArray[3] = 0x80 | (code & 0x3F) + } else if (code > 0x800) { + byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12) + byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6) + byteArray[2] = 0x80 | (code & 0x3F) + } else if (code > 0x80) { + byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6) + byteArray[1] = 0x80 | (code & 0x3F) + } else { + byteArray[0] = code + } + + this.parsedData.push(byteArray) + } + + this.parsedData = Array.prototype.concat.apply([], this.parsedData) + + if (this.parsedData.length != this.data.length) { + this.parsedData.unshift(191) + this.parsedData.unshift(187) + this.parsedData.unshift(239) + } +} + +QR8bitByte.prototype = { + getLength: function (buffer) { + return this.parsedData.length + }, + write: function (buffer) { + for (var i = 0, l = this.parsedData.length; i < l; i++) { + buffer.put(this.parsedData[i], 8) + } + } +} + +function QRCodeModel(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber + this.errorCorrectLevel = errorCorrectLevel + this.modules = null + this.moduleCount = 0 + this.dataCache = null + this.dataList = [] +} + +QRCodeModel.prototype = { + addData: function (data) { + var newData = new QR8bitByte(data) + this.dataList.push(newData) + this.dataCache = null + }, isDark: function (row, col) { + if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { + throw new Error(row + "," + col) + } + return this.modules[row][col] + }, getModuleCount: function () { + return this.moduleCount + }, make: function () { + this.makeImpl(false, this.getBestMaskPattern()) + }, makeImpl: function (test, maskPattern) { + this.moduleCount = this.typeNumber * 4 + 17 + this.modules = new Array(this.moduleCount) + for (var row = 0; row < this.moduleCount; row++) { + this.modules[row] = new Array(this.moduleCount) + for (var col = 0; col < this.moduleCount; col++) { + this.modules[row][col] = null + } + } + this.setupPositionProbePattern(0, 0) + this.setupPositionProbePattern(this.moduleCount - 7, 0) + this.setupPositionProbePattern(0, this.moduleCount - 7) + this.setupPositionAdjustPattern() + this.setupTimingPattern() + this.setupTypeInfo(test, maskPattern) + if (this.typeNumber >= 7) { + this.setupTypeNumber(test) + } + if (this.dataCache == null) { + this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList) + } + this.mapData(this.dataCache, maskPattern) + }, setupPositionProbePattern: function (row, col) { + for (var r = -1; r <= 7; r++) { + if (row + r <= -1 || this.moduleCount <= row + r) continue + for (var c = -1; c <= 7; c++) { + if (col + c <= -1 || this.moduleCount <= col + c) continue + if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) { + this.modules[row + r][col + c] = true + } else { + this.modules[row + r][col + c] = false + } + } + } + }, getBestMaskPattern: function () { + var minLostPoint = 0 + var pattern = 0 + for (var i = 0; i < 8; i++) { + this.makeImpl(true, i) + var lostPoint = QRUtil.getLostPoint(this) + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint + pattern = i + } + } + return pattern + }, createMovieClip: function (target_mc, instance_name, depth) { + var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth) + var cs = 1 + this.make() + for (var row = 0; row < this.modules.length; row++) { + var y = row * cs + for (var col = 0; col < this.modules[row].length; col++) { + var x = col * cs + var dark = this.modules[row][col] + if (dark) { + qr_mc.beginFill(0, 100) + qr_mc.moveTo(x, y) + qr_mc.lineTo(x + cs, y) + qr_mc.lineTo(x + cs, y + cs) + qr_mc.lineTo(x, y + cs) + qr_mc.endFill() + } + } + } + return qr_mc + }, setupTimingPattern: function () { + for (var r = 8; r < this.moduleCount - 8; r++) { + if (this.modules[r][6] != null) { + continue + } + this.modules[r][6] = (r % 2 == 0) + } + for (var c = 8; c < this.moduleCount - 8; c++) { + if (this.modules[6][c] != null) { + continue + } + this.modules[6][c] = (c % 2 == 0) + } + }, setupPositionAdjustPattern: function () { + var pos = QRUtil.getPatternPosition(this.typeNumber) + for (var i = 0; i < pos.length; i++) { + for (var j = 0; j < pos.length; j++) { + var row = pos[i] + var col = pos[j] + if (this.modules[row][col] != null) { + continue + } + for (var r = -2; r <= 2; r++) { + for (var c = -2; c <= 2; c++) { + if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) { + this.modules[row + r][col + c] = true + } else { + this.modules[row + r][col + c] = false + } + } + } + } + } + }, setupTypeNumber: function (test) { + var bits = QRUtil.getBCHTypeNumber(this.typeNumber) + for (var i = 0; i < 18; i++) { + var mod = (!test && ((bits >> i) & 1) == 1) + this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod + } + for (var i = 0; i < 18; i++) { + var mod = (!test && ((bits >> i) & 1) == 1) + this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod + } + }, setupTypeInfo: function (test, maskPattern) { + var data = (this.errorCorrectLevel << 3) | maskPattern + var bits = QRUtil.getBCHTypeInfo(data) + for (var i = 0; i < 15; i++) { + var mod = (!test && ((bits >> i) & 1) == 1) + if (i < 6) { + this.modules[i][8] = mod + } else if (i < 8) { + this.modules[i + 1][8] = mod + } else { + this.modules[this.moduleCount - 15 + i][8] = mod + } + } + for (var i = 0; i < 15; i++) { + var mod = (!test && ((bits >> i) & 1) == 1) + if (i < 8) { + this.modules[8][this.moduleCount - i - 1] = mod + } else if (i < 9) { + this.modules[8][15 - i - 1 + 1] = mod + } else { + this.modules[8][15 - i - 1] = mod + } + } + this.modules[this.moduleCount - 8][8] = (!test) + }, mapData: function (data, maskPattern) { + var inc = -1 + var row = this.moduleCount - 1 + var bitIndex = 7 + var byteIndex = 0 + for (var col = this.moduleCount - 1; col > 0; col -= 2) { + if (col == 6) col-- + while (true) { + for (var c = 0; c < 2; c++) { + if (this.modules[row][col - c] == null) { + var dark = false + if (byteIndex < data.length) { + dark = (((data[byteIndex] >>> bitIndex) & 1) == 1) + } + var mask = QRUtil.getMask(maskPattern, row, col - c) + if (mask) { + dark = !dark + } + this.modules[row][col - c] = dark + bitIndex-- + if (bitIndex == -1) { + byteIndex++ + bitIndex = 7 + } + } + } + row += inc + if (row < 0 || this.moduleCount <= row) { + row -= inc + inc = -inc + break + } + } + } + } +} +QRCodeModel.PAD0 = 0xEC +QRCodeModel.PAD1 = 0x11 +QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) { + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel) + var buffer = new QRBitBuffer() + for (var i = 0; i < dataList.length; i++) { + var data = dataList[i] + buffer.put(data.mode, 4) + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber)) + data.write(buffer) + } + var totalDataCount = 0 + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount + } + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw new Error("code length overflow. (" + + buffer.getLengthInBits() + + ">" + + totalDataCount * 8 + + ")") + } + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4) + } + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false) + } + while (true) { + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break + } + buffer.put(QRCodeModel.PAD0, 8) + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break + } + buffer.put(QRCodeModel.PAD1, 8) + } + return QRCodeModel.createBytes(buffer, rsBlocks) +} +QRCodeModel.createBytes = function (buffer, rsBlocks) { + var offset = 0 + var maxDcCount = 0 + var maxEcCount = 0 + var dcdata = new Array(rsBlocks.length) + var ecdata = new Array(rsBlocks.length) + for (var r = 0; r < rsBlocks.length; r++) { + var dcCount = rsBlocks[r].dataCount + var ecCount = rsBlocks[r].totalCount - dcCount + maxDcCount = Math.max(maxDcCount, dcCount) + maxEcCount = Math.max(maxEcCount, ecCount) + dcdata[r] = new Array(dcCount) + for (var i = 0; i < dcdata[r].length; i++) { + dcdata[r][i] = 0xff & buffer.buffer[i + offset] + } + offset += dcCount + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount) + var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1) + var modPoly = rawPoly.mod(rsPoly) + ecdata[r] = new Array(rsPoly.getLength() - 1) + for (var i = 0; i < ecdata[r].length; i++) { + var modIndex = i + modPoly.getLength() - ecdata[r].length + ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0 + } + } + var totalCodeCount = 0 + for (var i = 0; i < rsBlocks.length; i++) { + totalCodeCount += rsBlocks[i].totalCount + } + var data = new Array(totalCodeCount) + var index = 0 + for (var i = 0; i < maxDcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < dcdata[r].length) { + data[index++] = dcdata[r][i] + } + } + } + for (var i = 0; i < maxEcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < ecdata[r].length) { + data[index++] = ecdata[r][i] + } + } + } + return data +} +var QRMode = {MODE_NUMBER: 1 << 0, MODE_ALPHA_NUM: 1 << 1, MODE_8BIT_BYTE: 1 << 2, MODE_KANJI: 1 << 3} +var QRErrorCorrectLevel = {L: 1, M: 0, Q: 3, H: 2} +var QRMaskPattern = { + PATTERN000: 0, + PATTERN001: 1, + PATTERN010: 2, + PATTERN011: 3, + PATTERN100: 4, + PATTERN101: 5, + PATTERN110: 6, + PATTERN111: 7 +} +var QRUtil = { + PATTERN_POSITION_TABLE: [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]], + G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), + G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), + G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), + getBCHTypeInfo: function (data) { + var d = data << 10 + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { + d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15))) + } + return ((data << 10) | d) ^ QRUtil.G15_MASK + }, + getBCHTypeNumber: function (data) { + var d = data << 12 + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { + d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18))) + } + return (data << 12) | d + }, + getBCHDigit: function (data) { + var digit = 0 + while (data != 0) { + digit++ + data >>>= 1 + } + return digit + }, + getPatternPosition: function (typeNumber) { + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1] + }, + getMask: function (maskPattern, i, j) { + switch (maskPattern) { + case QRMaskPattern.PATTERN000: + return (i + j) % 2 == 0 + case QRMaskPattern.PATTERN001: + return i % 2 == 0 + case QRMaskPattern.PATTERN010: + return j % 3 == 0 + case QRMaskPattern.PATTERN011: + return (i + j) % 3 == 0 + case QRMaskPattern.PATTERN100: + return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0 + case QRMaskPattern.PATTERN101: + return (i * j) % 2 + (i * j) % 3 == 0 + case QRMaskPattern.PATTERN110: + return ((i * j) % 2 + (i * j) % 3) % 2 == 0 + case QRMaskPattern.PATTERN111: + return ((i * j) % 3 + (i + j) % 2) % 2 == 0 + default: + throw new Error("bad maskPattern:" + maskPattern) + } + }, + getErrorCorrectPolynomial: function (errorCorrectLength) { + var a = new QRPolynomial([1], 0) + for (var i = 0; i < errorCorrectLength; i++) { + a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0)) + } + return a + }, + getLengthInBits: function (mode, type) { + if (1 <= type && type < 10) { + switch (mode) { + case QRMode.MODE_NUMBER: + return 10 + case QRMode.MODE_ALPHA_NUM: + return 9 + case QRMode.MODE_8BIT_BYTE: + return 8 + case QRMode.MODE_KANJI: + return 8 + default: + throw new Error("mode:" + mode) + } + } else if (type < 27) { + switch (mode) { + case QRMode.MODE_NUMBER: + return 12 + case QRMode.MODE_ALPHA_NUM: + return 11 + case QRMode.MODE_8BIT_BYTE: + return 16 + case QRMode.MODE_KANJI: + return 10 + default: + throw new Error("mode:" + mode) + } + } else if (type < 41) { + switch (mode) { + case QRMode.MODE_NUMBER: + return 14 + case QRMode.MODE_ALPHA_NUM: + return 13 + case QRMode.MODE_8BIT_BYTE: + return 16 + case QRMode.MODE_KANJI: + return 12 + default: + throw new Error("mode:" + mode) + } + } else { + throw new Error("type:" + type) + } + }, + getLostPoint: function (qrCode) { + var moduleCount = qrCode.getModuleCount() + var lostPoint = 0 + for (var row = 0; row < moduleCount; row++) { + for (var col = 0; col < moduleCount; col++) { + var sameCount = 0 + var dark = qrCode.isDark(row, col) + for (var r = -1; r <= 1; r++) { + if (row + r < 0 || moduleCount <= row + r) { + continue + } + for (var c = -1; c <= 1; c++) { + if (col + c < 0 || moduleCount <= col + c) { + continue + } + if (r == 0 && c == 0) { + continue + } + if (dark == qrCode.isDark(row + r, col + c)) { + sameCount++ + } + } + } + if (sameCount > 5) { + lostPoint += (3 + sameCount - 5) + } + } + } + for (var row = 0; row < moduleCount - 1; row++) { + for (var col = 0; col < moduleCount - 1; col++) { + var count = 0 + if (qrCode.isDark(row, col)) count++ + if (qrCode.isDark(row + 1, col)) count++ + if (qrCode.isDark(row, col + 1)) count++ + if (qrCode.isDark(row + 1, col + 1)) count++ + if (count == 0 || count == 4) { + lostPoint += 3 + } + } + } + for (var row = 0; row < moduleCount; row++) { + for (var col = 0; col < moduleCount - 6; col++) { + if (qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6)) { + lostPoint += 40 + } + } + } + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount - 6; row++) { + if (qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col)) { + lostPoint += 40 + } + } + } + var darkCount = 0 + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount; row++) { + if (qrCode.isDark(row, col)) { + darkCount++ + } + } + } + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5 + lostPoint += ratio * 10 + return lostPoint + } +} +var QRMath = { + glog: function (n) { + if (n < 1) { + throw new Error("glog(" + n + ")") + } + return QRMath.LOG_TABLE[n] + }, gexp: function (n) { + while (n < 0) { + n += 255 + } + while (n >= 256) { + n -= 255 + } + return QRMath.EXP_TABLE[n] + }, EXP_TABLE: new Array(256), LOG_TABLE: new Array(256) +} +for (var i = 0; i < 8; i++) { + QRMath.EXP_TABLE[i] = 1 << i +} +for (var i = 8; i < 256; i++) { + QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8] +} +for (var i = 0; i < 255; i++) { + QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i +} + +function QRPolynomial(num, shift) { + if (num.length == undefined) { + throw new Error(num.length + "/" + shift) + } + var offset = 0 + while (offset < num.length && num[offset] == 0) { + offset++ + } + this.num = new Array(num.length - offset + shift) + for (var i = 0; i < num.length - offset; i++) { + this.num[i] = num[i + offset] + } +} + +QRPolynomial.prototype = { + get: function (index) { + return this.num[index] + }, getLength: function () { + return this.num.length + }, multiply: function (e) { + var num = new Array(this.getLength() + e.getLength() - 1) + for (var i = 0; i < this.getLength(); i++) { + for (var j = 0; j < e.getLength(); j++) { + num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j))) + } + } + return new QRPolynomial(num, 0) + }, mod: function (e) { + if (this.getLength() - e.getLength() < 0) { + return this + } + var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0)) + var num = new Array(this.getLength()) + for (var i = 0; i < this.getLength(); i++) { + num[i] = this.get(i) + } + for (var i = 0; i < e.getLength(); i++) { + num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio) + } + return new QRPolynomial(num, 0).mod(e) + } +} + +function QRRSBlock(totalCount, dataCount) { + this.totalCount = totalCount + this.dataCount = dataCount +} + +QRRSBlock.RS_BLOCK_TABLE = [[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], [1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], [1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], [1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], [1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12], [2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15], [2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14], [2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15], [2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13], [2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16], [4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13], [2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15], [4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12], [3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13], [5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12], [5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16], [1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15], [5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15], [3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14], [3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16], [4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17], [2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13], [4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16], [6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17], [8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16], [10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17], [8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16], [3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16], [7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16], [5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16], [13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16], [17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16], [17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16], [13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17], [12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16], [6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16], [17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16], [4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16], [20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16], [19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]] +QRRSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) { + var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel) + if (rsBlock == undefined) { + throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel) + } + var length = rsBlock.length / 3 + var list = [] + for (var i = 0; i < length; i++) { + var count = rsBlock[i * 3 + 0] + var totalCount = rsBlock[i * 3 + 1] + var dataCount = rsBlock[i * 3 + 2] + for (var j = 0; j < count; j++) { + list.push(new QRRSBlock(totalCount, dataCount)) + } + } + return list +} +QRRSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) { + switch (errorCorrectLevel) { + case QRErrorCorrectLevel.L: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0] + case QRErrorCorrectLevel.M: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1] + case QRErrorCorrectLevel.Q: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2] + case QRErrorCorrectLevel.H: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3] + default: + return undefined + } +} + +function QRBitBuffer() { + this.buffer = [] + this.length = 0 +} + +QRBitBuffer.prototype = { + get: function (index) { + var bufIndex = Math.floor(index / 8) + return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1 + }, put: function (num, length) { + for (var i = 0; i < length; i++) { + this.putBit(((num >>> (length - i - 1)) & 1) == 1) + } + }, getLengthInBits: function () { + return this.length + }, putBit: function (bit) { + var bufIndex = Math.floor(this.length / 8) + if (this.buffer.length <= bufIndex) { + this.buffer.push(0) + } + if (bit) { + this.buffer[bufIndex] |= (0x80 >>> (this.length % 8)) + } + this.length++ + } +} +var QRCodeLimitLength = [[17, 14, 11, 7], [32, 26, 20, 14], [53, 42, 32, 24], [78, 62, 46, 34], [106, 84, 60, 44], [134, 106, 74, 58], [154, 122, 86, 64], [192, 152, 108, 84], [230, 180, 130, 98], [271, 213, 151, 119], [321, 251, 177, 137], [367, 287, 203, 155], [425, 331, 241, 177], [458, 362, 258, 194], [520, 412, 292, 220], [586, 450, 322, 250], [644, 504, 364, 280], [718, 560, 394, 310], [792, 624, 442, 338], [858, 666, 482, 382], [929, 711, 509, 403], [1003, 779, 565, 439], [1091, 857, 611, 461], [1171, 911, 661, 511], [1273, 997, 715, 535], [1367, 1059, 751, 593], [1465, 1125, 805, 625], [1528, 1190, 868, 658], [1628, 1264, 908, 698], [1732, 1370, 982, 742], [1840, 1452, 1030, 790], [1952, 1538, 1112, 842], [2068, 1628, 1168, 898], [2188, 1722, 1228, 958], [2303, 1809, 1283, 983], [2431, 1911, 1351, 1051], [2563, 1989, 1423, 1093], [2699, 2099, 1499, 1139], [2809, 2213, 1579, 1219], [2953, 2331, 1663, 1273]] + +var useSVG = document.documentElement.tagName.toLowerCase() === "svg" + +let Drawing + +if (useSVG) { + + Drawing = function (el, htOption) { + this._el = el + this._htOption = htOption + } + + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption + var _el = this._el + var nCount = oQRCode.getModuleCount() + var nWidth = Math.floor(_htOption.width / nCount) + var nHeight = Math.floor(_htOption.height / nCount) + + this.clear() + + function makeSVG(tag, attrs) { + var el = document.createElementNS('http://www.w3.org/2000/svg', tag) + for (var k in attrs) + if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]) + return el + } + + var svg = makeSVG("svg", { + 'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), + 'width': '100%', + 'height': '100%', + 'fill': _htOption.colorLight + }) + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink") + _el.appendChild(svg) + + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})) + svg.appendChild(makeSVG("rect", { + "fill": _htOption.colorDark, + "width": "1", + "height": "1", + "id": "template" + })) + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + if (oQRCode.isDark(row, col)) { + var child = makeSVG("use", {"x": String(col), "y": String(row)}) + child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") + svg.appendChild(child) + } + } + } + } + Drawing.prototype.clear = function () { + while (this._el.hasChildNodes()) + this._el.removeChild(this._el.lastChild) + } +} else { + + /** + * Drawing QRCode by using canvas + * + * @constructor + * @param {HTMLElement} el + * @param {Object} htOption QRCode Options + */ + Drawing = function (el, htOption) { + this._bIsPainted = false + this._htOption = htOption + this._elCanvas = document.createElement("canvas") + this._elCanvas.width = htOption.width + this._elCanvas.height = htOption.height + el.appendChild(this._elCanvas) + this._el = el + this._oContext = this._elCanvas.getContext("2d") + this._bIsPainted = false + this._elImage = document.createElement("img") + this._elImage.alt = "Scan me!" + this._elImage.style.display = "none" + this._el.appendChild(this._elImage) + this._bSupportDataURI = null + } + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _elImage = this._elImage + var _oContext = this._oContext + var _htOption = this._htOption + + var nCount = oQRCode.getModuleCount() + var nWidth = _htOption.width / nCount + var nHeight = _htOption.height / nCount + var nRoundedWidth = Math.round(nWidth) + var nRoundedHeight = Math.round(nHeight) + + _elImage.style.display = "none" + this.clear() + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + var bIsDark = oQRCode.isDark(row, col) + var nLeft = col * nWidth + var nTop = row * nHeight + _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight + _oContext.lineWidth = 1 + _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight + _oContext.fillRect(nLeft, nTop, nWidth, nHeight) + + // 안티 앨리어싱 방지 처리 + _oContext.strokeRect( + Math.floor(nLeft) + 0.5, + Math.floor(nTop) + 0.5, + nRoundedWidth, + nRoundedHeight + ) + + _oContext.strokeRect( + Math.ceil(nLeft) - 0.5, + Math.ceil(nTop) - 0.5, + nRoundedWidth, + nRoundedHeight + ) + } + } + + this._bIsPainted = true + } + + /** + * Make the image from Canvas if the browser supports Data URI. + */ + Drawing.prototype.makeImage = function () { + if (this._bIsPainted) { + this._elImage.src = this._elCanvas.toDataURL("image/png") + this._elImage.style.display = "block" + this._elCanvas.style.display = "none" + } + } + + /** + * Return whether the QRCode is painted or not + * + * @return {Boolean} + */ + Drawing.prototype.isPainted = function () { + return this._bIsPainted + } + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height) + this._bIsPainted = false + } + + /** + * @private + * @param {Number} nNumber + */ + Drawing.prototype.round = function (nNumber) { + if (!nNumber) { + return nNumber + } + + return Math.floor(nNumber * 1000) / 1000 + } +} + +/** + * Get the type by string length + * + * @private + * @param {String} sText + * @param {Number} nCorrectLevel + * @return {Number} type + */ +function _getTypeNumber(sText, nCorrectLevel) { + var nType = 1 + var length = _getUTF8Length(sText) + + for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { + var nLimit = 0 + + switch (nCorrectLevel) { + case QRErrorCorrectLevel.L : + nLimit = QRCodeLimitLength[i][0] + break + case QRErrorCorrectLevel.M : + nLimit = QRCodeLimitLength[i][1] + break + case QRErrorCorrectLevel.Q : + nLimit = QRCodeLimitLength[i][2] + break + case QRErrorCorrectLevel.H : + nLimit = QRCodeLimitLength[i][3] + break + } + + if (length <= nLimit) { + break + } else { + nType++ + } + } + + if (nType > QRCodeLimitLength.length) { + throw new Error("Too long data") + } + + return nType +} + +function _getUTF8Length(sText) { + var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a') + return replacedText.length + (replacedText.length != sText ? 3 : 0) +} + +/** + * @class QRCode + * @constructor + * @example + * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); + * + * @example + * var oQRCode = new QRCode("test", { + * text : "http://naver.com", + * width : 128, + * height : 128 + * }); + * + * oQRCode.clear(); // Clear the QRCode. + * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. + * + * @param {HTMLElement|String} el target element or 'id' attribute of element. + * @param {Object|String} vOption + * @param {String} vOption.text QRCode link data + * @param {Number} [vOption.width=256] + * @param {Number} [vOption.height=256] + * @param {String} [vOption.colorDark="#000000"] + * @param {String} [vOption.colorLight="#ffffff"] + * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] + */ +const QRCode = function (el, vOption) { + this._htOption = { + width: 256, + height: 256, + typeNumber: 4, + colorDark: "#000000", + colorLight: "#ffffff", + correctLevel: QRErrorCorrectLevel.H + } + + if (typeof vOption === 'string') { + vOption = { + text: vOption + } + } + + // Overwrites options + if (vOption) { + for (var i in vOption) { + this._htOption[i] = vOption[i] + } + } + + if (typeof el == "string") { + el = document.getElementById(el) + } + + if (this._htOption.useSVG) { + Drawing = svgDrawer + } + + this._el = el + this._oQRCode = null + this._oDrawing = new Drawing(this._el, this._htOption) + + if (this._htOption.text) { + this.makeCode(this._htOption.text) + } +} + +/** + * Make the QRCode + * + * @param {String} sText link data + */ +QRCode.prototype.makeCode = function (sText) { + this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel) + this._oQRCode.addData(sText) + this._oQRCode.make() + this._el.title = sText + this._oDrawing.draw(this._oQRCode) + this.makeImage() +} + +/** + * Make the Image from Canvas element + * - It occurs automatically + * - Android below 3 doesn't support Data-URI spec. + * + * @private + */ +QRCode.prototype.makeImage = function () { + if (typeof this._oDrawing.makeImage == "function") { + this._oDrawing.makeImage() + } +} + +/** + * Clear the QRCode + */ +QRCode.prototype.clear = function () { + this._oDrawing.clear() +} + +/** + * @name QRCode.CorrectLevel + */ +QRCode.CorrectLevel = QRErrorCorrectLevel + +export {QRCode} \ No newline at end of file diff --git a/js/widgets/sessionController.js b/js/widgets/sessionController.js new file mode 100644 index 00000000..5c5730b4 --- /dev/null +++ b/js/widgets/sessionController.js @@ -0,0 +1,79 @@ +import FileLoadWidget from "./fileLoadWidget.js" +import FileLoadManager from "./fileLoadManager.js" +import * as Utils from './utils.js' +import {FileUtils} from '../node_modules/igv-utils/src/index.js' + +class SessionController { + + constructor({prefix, sessionLoadModal, sessionSaveModal, sessionFileLoad, JSONProvider}) { + + let config = + { + widgetParent: sessionLoadModal.querySelector('.modal-body'), + dataTitle: 'Load Session', + indexTitle: undefined, + mode: 'url', + fileLoadManager: new FileLoadManager(), + dataOnly: true, + doURL: undefined + } + + this.urlWidget = new FileLoadWidget(config) + + // Configure load session modal + Utils.configureModal(this.urlWidget, sessionLoadModal, async fileLoadWidget => { + await sessionFileLoad.loadPaths(fileLoadWidget.retrievePaths()) + return true + }) + + // Configure save session modal + configureSaveSessionModal(prefix, JSONProvider, sessionSaveModal) + + } + +} + + +function configureSaveSessionModal(prefix, JSONProvider, sessionSaveModal) { + + let input = sessionSaveModal.querySelector('input') + + let okHandler = () => { + + const extensions = new Set(['json', 'xml']) + + let filename = input.value + + if (undefined === filename || '' === filename) { + filename = input.getAttribute('placeholder') + } else if (false === extensions.has(FileUtils.getExtension(filename))) { + filename = filename + '.json' + } + + const json = JSONProvider() + const jsonString = JSON.stringify(json, null, '\t') + const data = URL.createObjectURL(new Blob([jsonString], {type: "application/octet-stream"})) + + FileUtils.download(filename, data) + + $(sessionSaveModal).modal('hide') + } + + const $ok = $(sessionSaveModal).find('.modal-footer button:nth-child(2)') + $ok.on('click', okHandler) + + $(sessionSaveModal).on('show.bs.modal', (e) => { + input.value = `${prefix}-session.json` + }) + + input.addEventListener('keyup', e => { + + // enter key key-up + if (13 === e.keyCode) { + okHandler() + } + }) + +} + +export default SessionController diff --git a/js/widgets/sessionFileLoad.js b/js/widgets/sessionFileLoad.js new file mode 100644 index 00000000..a07cf142 --- /dev/null +++ b/js/widgets/sessionFileLoad.js @@ -0,0 +1,24 @@ +import FileLoad from "./fileLoad.js" + +class SessionFileLoad extends FileLoad { + + constructor({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton, loadHandler}) { + super({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton}) + this.loadHandler = loadHandler + } + + async loadPaths(paths) { + + const path = paths[0] + + try { + this.loadHandler({url: path}) + + } catch (e) { + throw new Error('Session file did not load' + e.message) + } + }; + +} + +export default SessionFileLoad diff --git a/js/widgets/sessionWidgets.js b/js/widgets/sessionWidgets.js new file mode 100644 index 00000000..da74136f --- /dev/null +++ b/js/widgets/sessionWidgets.js @@ -0,0 +1,150 @@ +import {FileUtils} from '../../node_modules/igv-utils/src/index.js' +import FileLoadManager from './fileLoadManager.js' +import FileLoadWidget from './fileLoadWidget.js' +import SessionFileLoad from "./sessionFileLoad.js" +import {createURLModal} from './urlModal.js' +import * as Utils from './utils.js' + +let fileLoadWidget + +function createSessionWidgets($rootContainer, + prefix, + localFileInputId, + initializeDropbox, + dropboxButtonId, + googleDriveButtonId, + urlModalId, + sessionSaveModalId, + googleEnabled, + loadHandler, + JSONProvider) { + + const urlModal = createURLModal(urlModalId, 'Session URL') + $rootContainer.get(0).appendChild(urlModal) + + if (!googleEnabled) { + $(`#${googleDriveButtonId}`).parent().hide() + } + + const fileLoadWidgetConfig = + { + widgetParent: urlModal.querySelector('.modal-body'), + dataTitle: 'Session', + indexTitle: undefined, + mode: 'url', + fileLoadManager: new FileLoadManager(), + dataOnly: true, + doURL: undefined + } + + fileLoadWidget = new FileLoadWidget(fileLoadWidgetConfig) + + const sessionFileLoadConfig = + { + localFileInput: document.querySelector(`#${localFileInputId}`), + initializeDropbox, + dropboxButton: dropboxButtonId ? document.querySelector(`#${dropboxButtonId}`) : undefined, + googleEnabled, + googleDriveButton: document.querySelector(`#${googleDriveButtonId}`), + loadHandler + } + + const sessionFileLoad = new SessionFileLoad(sessionFileLoadConfig) + + Utils.configureModal(fileLoadWidget, urlModal, async fileLoadWidget => { + await sessionFileLoad.loadPaths(fileLoadWidget.retrievePaths()) + return true + }) + + configureSaveSessionModal($rootContainer, prefix, JSONProvider, sessionSaveModalId) + +} + +function configureSaveSessionModal($rootContainer, prefix, JSONProvider, sessionSaveModalId) { + + const modal = + `` + + const $modal = $(modal) + $rootContainer.append($modal) + + let $input = $modal.find('input') + + let okHandler = () => { + + const extensions = new Set(['json', 'xml']) + + let filename = $input.val() + + if (undefined === filename || '' === filename) { + filename = $input.attr('placeholder') + } else if (false === extensions.has(FileUtils.getExtension(filename))) { + filename = filename + '.json' + } + + const json = JSONProvider() + + if (json) { + const jsonString = JSON.stringify(json, null, '\t') + const data = URL.createObjectURL(new Blob([jsonString], {type: "application/octet-stream"})) + FileUtils.download(filename, data) + } + + $modal.modal('hide') + } + + const $ok = $modal.find('.modal-footer button:nth-child(2)') + $ok.on('click', okHandler) + + $modal.on('show.bs.modal', (e) => { + $input.val(`${prefix}-session.json`) + }) + + $input.on('keyup', e => { + + // enter key + if (13 === e.keyCode) { + okHandler() + } + }) + +} + +export {createSessionWidgets} diff --git a/js/widgets/trackURLModal.js b/js/widgets/trackURLModal.js new file mode 100644 index 00000000..6ef16201 --- /dev/null +++ b/js/widgets/trackURLModal.js @@ -0,0 +1,39 @@ +const createTrackURLModal = id => { + + const html = + `` + + const fragment = document.createRange().createContextualFragment(html) + + return fragment.firstChild + +} + +export {createTrackURLModal} diff --git a/js/widgets/trackWidgets.js b/js/widgets/trackWidgets.js new file mode 100644 index 00000000..c7d8b8a8 --- /dev/null +++ b/js/widgets/trackWidgets.js @@ -0,0 +1,434 @@ +import {ModalTable, GenericDataSource} from '../../node_modules/data-modal/src/index.js' +import {encodeTrackDatasourceConfigurator, supportsGenome} from './encodeTrackDatasourceConfigurator.js' +import AlertSingleton from './alertSingleton.js' +import {createGenericSelectModal} from './genericSelectModal.js' +import {createTrackURLModal} from './trackURLModal.js' +import FileLoadManager from "./fileLoadManager.js" +import FileLoadWidget from "./fileLoadWidget.js" +import MultipleTrackFileLoad from "./multipleTrackFileLoad.js" +import * as Utils from './utils.js' + +let fileLoadWidget +let multipleTrackFileLoad +let encodeModalTables = [] +let customModalTable +let $genericSelectModal = undefined + +const defaultCustomModalTableConfig = + { + // id: modalID, + // title: 'ENCODE', + selectionStyle: 'multi', + pageLength: 100 + } + +function createTrackWidgetsWithTrackRegistry($igvMain, + $dropdownMenu, + $localFileInput, + initializeDropbox, + $dropboxButton, + googleEnabled, + $googleDriveButton, + encodeTrackModalIds, + urlModalId, + selectModalIdOrUndefined, + GtexUtilsOrUndefined, + trackRegistryFile, + trackLoadHandler, + trackMenuHandler) { + + const urlModal = createTrackURLModal(urlModalId) + $igvMain.get(0).appendChild(urlModal) + + let fileLoadWidgetConfig = + { + widgetParent: urlModal.querySelector('.modal-body'), + dataTitle: 'Track', + indexTitle: 'Index', + mode: 'url', + fileLoadManager: new FileLoadManager(), + dataOnly: false, + doURL: true + } + + fileLoadWidget = new FileLoadWidget(fileLoadWidgetConfig) + + Utils.configureModal(fileLoadWidget, urlModal, async fileLoadWidget => { + const paths = fileLoadWidget.retrievePaths() + await multipleTrackFileLoad.loadPaths(paths) + return true + }) + + if ($googleDriveButton && !googleEnabled) { + $googleDriveButton.parent().hide() + } + + const multipleTrackFileLoadConfig = + { + $localFileInput, + initializeDropbox, + $dropboxButton, + $googleDriveButton: googleEnabled ? $googleDriveButton : undefined, + fileLoadHandler: trackLoadHandler, + multipleFileSelection: true + } + + multipleTrackFileLoad = new MultipleTrackFileLoad(multipleTrackFileLoadConfig) + + for (let modalID of encodeTrackModalIds) { + + const encodeModalTableConfig = + { + id: modalID, + title: 'ENCODE', + selectionStyle: 'multi', + pageLength: 100, + okHandler: trackLoadHandler + } + + encodeModalTables.push(new ModalTable(encodeModalTableConfig)) + + } + + customModalTable = new ModalTable({ + id: 'igv-custom-modal', + title: 'UNTITLED', + okHandler: trackLoadHandler, ...defaultCustomModalTableConfig + }) + + if (selectModalIdOrUndefined) { + createGenericSelectModalWidget($igvMain, selectModalIdOrUndefined, trackLoadHandler, trackMenuHandler) + } + +} + +function createGenericSelectModalWidget($igvMain, selectModalIdOrUndefined, trackLoadHandler, trackMenuHandler) { + + $genericSelectModal = $(createGenericSelectModal(selectModalIdOrUndefined, `${selectModalIdOrUndefined}-select`)) + + $igvMain.append($genericSelectModal) + const $select = $genericSelectModal.find('select') + + const $dismiss = $genericSelectModal.find('.modal-footer button:nth-child(1)') + $dismiss.on('click', () => $genericSelectModal.modal('hide')) + + const $ok = $genericSelectModal.find('.modal-footer button:nth-child(2)') + + const okHandler = () => { + + const configurations = [] + const $selectedOptions = $select.find('option:selected') + $selectedOptions.each(function () { + // console.log(`${ $(this).val() } was selected`) + configurations.push($(this).data('track')) + $(this).removeAttr('selected') + }) + + if (configurations.length > 0) { + trackLoadHandler(configurations) + } + + $genericSelectModal.modal('hide') + + } + + $ok.on('click', okHandler) + + $genericSelectModal.get(0).addEventListener('keypress', event => { + if ('Enter' === event.key) { + okHandler() + } + }) + + $genericSelectModal.on('show.bs.modal', () => { + + const urlList = [] + $genericSelectModal.find('select').find('option').each(function () { + + const {url} = $(this).data('track') + urlList.push({element: $(this).get(0), url}) + }) + + trackMenuHandler(urlList) + + }) + + +} + +async function updateTrackMenusWithTrackConfigurations(genomeID, GtexUtilsOrUndefined, trackConfigurations, $dropdownMenu) { + + const id_prefix = 'genome_specific_' + + const $divider = $dropdownMenu.find('.dropdown-divider') + + const searchString = '[id^=' + id_prefix + ']' + const $found = $dropdownMenu.find(searchString) + $found.remove() + + let buttonConfigurations = [] + + for (const trackConfiguration of trackConfigurations) { + + if (true === supportsGenome(genomeID) && 'ENCODE' === trackConfiguration.type) { + encodeModalTables[0].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-chip'))) + encodeModalTables[1].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-other'))) + encodeModalTables[2].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'other'))) + } else if (GtexUtilsOrUndefined && 'GTEX' === trackConfiguration.type) { + + let info = undefined + try { + info = await GtexUtilsOrUndefined.getTissueInfo(trackConfiguration.datasetId) + } catch (e) { + AlertSingleton.present(e.message) + } + + if (info) { + trackConfiguration.tracks = info.tissueInfo.map(tissue => GtexUtilsOrUndefined.trackConfiguration(tissue)) + } + + } + + buttonConfigurations.push(trackConfiguration) + + } // for(jsons) + + for (let buttonConfiguration of buttonConfigurations.reverse()) { + + if (buttonConfiguration.type && 'custom-data-modal' === buttonConfiguration.type) { + + createDropdownButton($divider, buttonConfiguration.label, id_prefix) + .on('click', () => { + + if (buttonConfiguration.description) { + customModalTable.setDescription(buttonConfiguration.description) + } + + customModalTable.setDatasource(new GenericDataSource(buttonConfiguration)) + customModalTable.setTitle(buttonConfiguration.label) + customModalTable.$modal.modal('show') + }) + + } else if (buttonConfiguration.type && 'ENCODE' === buttonConfiguration.type) { + + if (true === supportsGenome(genomeID)) { + + if (buttonConfiguration.description) { + encodeModalTables[0].setDescription(buttonConfiguration.description) + encodeModalTables[1].setDescription(buttonConfiguration.description) + encodeModalTables[2].setDescription(buttonConfiguration.description) + } + + createDropdownButton($divider, 'ENCODE Other', id_prefix) + .on('click', () => encodeModalTables[2].$modal.modal('show')) + + createDropdownButton($divider, 'ENCODE Signals - Other', id_prefix) + .on('click', () => encodeModalTables[1].$modal.modal('show')) + + createDropdownButton($divider, 'ENCODE Signals - ChIP', id_prefix) + .on('click', () => encodeModalTables[0].$modal.modal('show')) + + } + + } else if ($genericSelectModal) { + + createDropdownButton($divider, buttonConfiguration.label, id_prefix) + .on('click', () => { + configureSelectModal($genericSelectModal, buttonConfiguration) + $genericSelectModal.modal('show') + }) + + } + } // for (buttonConfigurations) + +} + +async function updateTrackMenus(genomeID, GtexUtilsOrUndefined, trackRegistryFile, $dropdownMenu) { + + const id_prefix = 'genome_specific_' + + const $divider = $dropdownMenu.find('.dropdown-divider') + + const searchString = '[id^=' + id_prefix + ']' + const $found = $dropdownMenu.find(searchString) + $found.remove() + + const paths = await getPathsWithTrackRegistryFile(genomeID, trackRegistryFile) + + if (undefined === paths) { + console.warn(`There are no tracks in the track registry for genome ${genomeID}`) + return + } + + let responses = [] + try { + responses = await Promise.all(paths.map(path => fetch(path))) + } catch (e) { + AlertSingleton.present(e.message) + } + + let jsons = [] + try { + jsons = await Promise.all(responses.map(response => response.json())) + } catch (e) { + AlertSingleton.present(e.message) + } + + let buttonConfigurations = [] + + for (let json of jsons) { + + if (true === supportsGenome(genomeID) && 'ENCODE' === json.type) { + encodeModalTables[0].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-chip'))) + encodeModalTables[1].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-other'))) + encodeModalTables[2].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'other'))) + } else if (GtexUtilsOrUndefined && 'GTEX' === json.type) { + + let info = undefined + try { + info = await GtexUtilsOrUndefined.getTissueInfo(json.datasetId) + } catch (e) { + AlertSingleton.present(e.message) + } + + if (info) { + json.tracks = info.tissueInfo.map(tissue => GtexUtilsOrUndefined.trackConfiguration(tissue)) + } + + } + + buttonConfigurations.push(json) + + } // for(jsons) + + for (let buttonConfiguration of buttonConfigurations.reverse()) { + + if (buttonConfiguration.type && 'custom-data-modal' === buttonConfiguration.type) { + + createDropdownButton($divider, buttonConfiguration.label, id_prefix) + .on('click', () => { + + if (buttonConfiguration.description) { + customModalTable.setDescription(buttonConfiguration.description) + } + + customModalTable.setDatasource(new GenericDataSource(buttonConfiguration)) + customModalTable.setTitle(buttonConfiguration.label) + customModalTable.$modal.modal('show') + }) + + } else if (buttonConfiguration.type && 'ENCODE' === buttonConfiguration.type) { + + if (true === supportsGenome(genomeID)) { + + if (buttonConfiguration.description) { + encodeModalTables[0].setDescription(buttonConfiguration.description) + encodeModalTables[1].setDescription(buttonConfiguration.description) + encodeModalTables[2].setDescription(buttonConfiguration.description) + } + + createDropdownButton($divider, 'ENCODE Other', id_prefix) + .on('click', () => encodeModalTables[2].$modal.modal('show')) + + createDropdownButton($divider, 'ENCODE Signals - Other', id_prefix) + .on('click', () => encodeModalTables[1].$modal.modal('show')) + + createDropdownButton($divider, 'ENCODE Signals - ChIP', id_prefix) + .on('click', () => encodeModalTables[0].$modal.modal('show')) + + } + + } else if ($genericSelectModal) { + + createDropdownButton($divider, buttonConfiguration.label, id_prefix) + .on('click', () => { + configureSelectModal($genericSelectModal, buttonConfiguration) + $genericSelectModal.modal('show') + }) + + } + } // for (buttonConfigurations) + +} + +function createDropdownButton($divider, buttonText, id_prefix) { + const $button = $(' + + + + + + + + + + + + ` + + const fragment = document.createRange().createContextualFragment(html) + + return fragment.firstChild +} + +export {createURLModal} diff --git a/js/widgets/utils.js b/js/widgets/utils.js new file mode 100644 index 00000000..c1fca27b --- /dev/null +++ b/js/widgets/utils.js @@ -0,0 +1,40 @@ +function configureModal(fileLoadWidget, modal, okHandler) { + + const doDismiss = () => { + fileLoadWidget.dismiss() + $(modal).modal('hide') + } + + const doOK = async () => { + + const result = await okHandler(fileLoadWidget) + + if (true === result) { + fileLoadWidget.dismiss() + $(modal).modal('hide') + } + } + + let dismiss + + // upper dismiss - x - button + dismiss = modal.querySelector('.modal-header button') + dismiss.addEventListener('click', doDismiss) + + // lower dismiss - close - button + dismiss = modal.querySelector('.modal-footer button:nth-child(1)') + dismiss.addEventListener('click', doDismiss) + + // ok - button + const ok = modal.querySelector('.modal-footer button:nth-child(2)') + + ok.addEventListener('click', doOK) + + modal.addEventListener('keypress', event => { + if ('Enter' === event.key) { + doOK() + } + }) +} + +export {configureModal} diff --git a/js/widgets/utils/dom-utils.js b/js/widgets/utils/dom-utils.js new file mode 100644 index 00000000..42e7b6bd --- /dev/null +++ b/js/widgets/utils/dom-utils.js @@ -0,0 +1,120 @@ +function div(options) { + return create("div", options) +} + +function create(tag, options) { + const elem = document.createElement(tag) + if (options) { + if (options.class) { + elem.classList.add(options.class) + } + if (options.id) { + elem.id = options.id + } + if (options.style) { + applyStyle(elem, options.style) + } + } + return elem +} + +function hide(elem) { + const cssStyle = getComputedStyle(elem) + if (cssStyle.display !== "none") { + elem._initialDisplay = cssStyle.display + } + elem.style.display = "none" +} + +function show(elem) { + const currentDisplay = getComputedStyle(elem).display + if (currentDisplay === "none") { + const d = elem._initialDisplay || "block" + elem.style.display = d + } +} + +function hideAll(selector) { + document.querySelectorAll(selector).forEach(elem => { + hide(elem) + }) +} + +function empty(elem) { + while (elem.firstChild) { + elem.removeChild(elem.firstChild) + } +} + +function offset(elem) { + // Return zeros for disconnected and hidden (display: none) elements (gh-2310) + // Support: IE <=11 only + // Running getBoundingClientRect on a + // disconnected node in IE throws an error + if (!elem.getClientRects().length) { + return {top: 0, left: 0} + } + + // Get document-relative position by adding viewport scroll to viewport-relative gBCR + const rect = elem.getBoundingClientRect() + const win = elem.ownerDocument.defaultView + return { + top: rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset + } +} + +function pageCoordinates(e) { + + if (e.type.startsWith("touch")) { + const touch = e.touches[0] + return {x: touch.pageX, y: touch.pageY} + } else { + return {x: e.pageX, y: e.pageY} + } +} + +const relativeDOMBBox = (parentElement, childElement) => { + const {x: x_p, y: y_p, width: width_p, height: height_p} = parentElement.getBoundingClientRect() + const {x: x_c, y: y_c, width: width_c, height: height_c} = childElement.getBoundingClientRect() + return {x: (x_c - x_p), y: (y_c - y_p), width: width_c, height: height_c} +} + +function applyStyle(elem, style) { + for (let key of Object.keys(style)) { + elem.style[key] = style[key] + } +} + +function guid() { + return ("0000" + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4) +} + +let getMouseXY = (domElement, {clientX, clientY}) => { + + // DOMRect object with eight properties: left, top, right, bottom, x, y, width, height + const {left, top, width, height} = domElement.getBoundingClientRect() + + const x = clientX - left + const y = clientY - top + return {x, y, xNormalized: x / width, yNormalized: y / height, width, height} + +} + +/** + * Translate the mouse coordinates for the event to the coordinates for the given target element + * @param event + * @param domElement + * @returns {{x: number, y: number}} + */ +function translateMouseCoordinates(event, domElement) { + + const {clientX, clientY} = event + return getMouseXY(domElement, {clientX, clientY}) + +} + +export { + create, div, hide, show, offset, hideAll, empty, pageCoordinates, relativeDOMBBox, + applyStyle, guid, translateMouseCoordinates +} diff --git a/js/draggable.js b/js/widgets/utils/draggable.js similarity index 77% rename from js/draggable.js rename to js/widgets/utils/draggable.js index 37dcae46..408450d7 100644 --- a/js/draggable.js +++ b/js/widgets/utils/draggable.js @@ -5,7 +5,7 @@ */ -let _dragData // Its assumed we are only dragging one element at a time. +let dragData // Its assumed we are only dragging one element at a time. function makeDraggable(target, handle, constraint) { @@ -20,7 +20,7 @@ function makeDraggable(target, handle, constraint) { const dragEndFunction = dragEnd.bind(this) const computedStyle = getComputedStyle(this) - _dragData = + dragData = { constraint, dragFunction, @@ -40,17 +40,17 @@ function makeDraggable(target, handle, constraint) { function drag(event) { - if (!_dragData) { + if (!dragData) { console.error("No drag data!") return } event.stopPropagation() event.preventDefault() - const dx = event.screenX - _dragData.screenX - const dy = event.screenY - _dragData.screenY + const dx = event.screenX - dragData.screenX + const dy = event.screenY - dragData.screenY - const left = _dragData.left + dx - const top = _dragData.constraint ? Math.max(_dragData.constraint.minY, _dragData.top + dy) : _dragData.top + dy + const left = dragData.left + dx + const top = dragData.constraint ? Math.max(dragData.constraint.minY, dragData.top + dy) : dragData.top + dy this.style.left = `${left}px` this.style.top = `${top}px` @@ -58,20 +58,20 @@ function drag(event) { function dragEnd(event) { - if (!_dragData) { + if (!dragData) { console.error("No drag data!") return } event.stopPropagation() event.preventDefault() - const dragFunction = _dragData.dragFunction - const dragEndFunction = _dragData.dragEndFunction + const dragFunction = dragData.dragFunction + const dragEndFunction = dragData.dragEndFunction document.removeEventListener('mousemove', dragFunction) document.removeEventListener('mouseup', dragEndFunction) document.removeEventListener('mouseleave', dragEndFunction) document.removeEventListener('mouseexit', dragEndFunction) - _dragData = undefined + dragData = undefined } -export {makeDraggable} +export default makeDraggable diff --git a/js/widgets/utils/icons.js b/js/widgets/utils/icons.js new file mode 100644 index 00000000..7b68328e --- /dev/null +++ b/js/widgets/utils/icons.js @@ -0,0 +1,48 @@ +function createIcon(name, color) { + return iconMarkup(name, color) +} + +function iconMarkup(name, color) { + color = color || "currentColor" + let icon = icons[name] + if (!icon) { + console.error(`No icon named: ${name}`) + icon = icons["question"] + } + + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") + svg.setAttributeNS(null, 'viewBox', '0 0 ' + icon[0] + ' ' + icon[1]) + const path = document.createElementNS("http://www.w3.org/2000/svg", "path") + path.setAttributeNS(null, 'fill', color) + path.setAttributeNS(null, 'd', icon[4]) + svg.appendChild(path) + return svg +} + +const icons = { + "check": [512, 512, [], "f00c", "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"], + "cog": [512, 512, [], "f013", "M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z"], + "exclamation": [192, 512, [], "f12a", "M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z"], + "exclamation-circle": [512, 512, [], "f06a", "M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"], + "exclamation-triangle": [576, 512, [], "f071", "M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"], + "minus": [448, 512, [], "f068", "M424 318.2c13.3 0 24-10.7 24-24v-76.4c0-13.3-10.7-24-24-24H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h400z"], + "minus-circle": [512, 512, [], "f056", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zM124 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H124z"], + "minus-square": [448, 512, [], "f146", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM92 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H92z"], + "plus": [448, 512, [], "f067", "M448 294.2v-76.4c0-13.3-10.7-24-24-24H286.2V56c0-13.3-10.7-24-24-24h-76.4c-13.3 0-24 10.7-24 24v137.8H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h137.8V456c0 13.3 10.7 24 24 24h76.4c13.3 0 24-10.7 24-24V318.2H424c13.3 0 24-10.7 24-24z"], + "plus-circle": [512, 512, [], "f055", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"], + "plus-square": [448, 512, [], "f0fe", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-32 252c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92H92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"], + "question": [384, 512, [], "f128", "M202.021 0C122.202 0 70.503 32.703 29.914 91.026c-7.363 10.58-5.093 25.086 5.178 32.874l43.138 32.709c10.373 7.865 25.132 6.026 33.253-4.148 25.049-31.381 43.63-49.449 82.757-49.449 30.764 0 68.816 19.799 68.816 49.631 0 22.552-18.617 34.134-48.993 51.164-35.423 19.86-82.299 44.576-82.299 106.405V320c0 13.255 10.745 24 24 24h72.471c13.255 0 24-10.745 24-24v-5.773c0-42.86 125.268-44.645 125.268-160.627C377.504 66.256 286.902 0 202.021 0zM192 373.459c-38.196 0-69.271 31.075-69.271 69.271 0 38.195 31.075 69.27 69.271 69.27s69.271-31.075 69.271-69.271-31.075-69.27-69.271-69.27z"], + "save": [448, 512, [], "f0c7", "M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"], + "search": [512, 512, [], "f002", "M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"], + "share": [512, 512, [], "f064", "M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z"], + "spinner": [512, 512, [], "f110", "M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"], + "square": [448, 512, [], "f0c8", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"], + "square-full": [512, 512, [], "f45c", "M512 512H0V0h512v512z"], + "times": [384, 512, [], "f00d", "M323.1 441l53.9-53.9c9.4-9.4 9.4-24.5 0-33.9L279.8 256l97.2-97.2c9.4-9.4 9.4-24.5 0-33.9L323.1 71c-9.4-9.4-24.5-9.4-33.9 0L192 168.2 94.8 71c-9.4-9.4-24.5-9.4-33.9 0L7 124.9c-9.4 9.4-9.4 24.5 0 33.9l97.2 97.2L7 353.2c-9.4 9.4-9.4 24.5 0 33.9L60.9 441c9.4 9.4 24.5 9.4 33.9 0l97.2-97.2 97.2 97.2c9.3 9.3 24.5 9.3 33.9 0z"], + "times-circle": [512, 512, [], "f057", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"], + "wrench": [512, 512, [], "f0ad", "M481.156 200c9.3 0 15.12 10.155 10.325 18.124C466.295 259.992 420.419 288 368 288c-79.222 0-143.501-63.974-143.997-143.079C223.505 65.469 288.548-.001 368.002 0c52.362.001 98.196 27.949 123.4 69.743C496.24 77.766 490.523 88 481.154 88H376l-40 56 40 56h105.156zm-171.649 93.003L109.255 493.255c-24.994 24.993-65.515 24.994-90.51 0-24.993-24.994-24.993-65.516 0-90.51L218.991 202.5c16.16 41.197 49.303 74.335 90.516 90.503zM104 432c0-13.255-10.745-24-24-24s-24 10.745-24 24 10.745 24 24 24 24-10.745 24-24z"], +} + +export {createIcon} + + diff --git a/js/widgets/utils/ui-utils.js b/js/widgets/utils/ui-utils.js new file mode 100644 index 00000000..99a61722 --- /dev/null +++ b/js/widgets/utils/ui-utils.js @@ -0,0 +1,16 @@ +import {createIcon} from "./icons.js" + +function attachDialogCloseHandlerWithParent(parent, closeHandler) { + + var container = document.createElement("div") + parent.appendChild(container) + container.appendChild(createIcon("times")) + container.addEventListener('click', function (e) { + e.preventDefault() + e.stopPropagation() + closeHandler() + }) +} + +export {attachDialogCloseHandlerWithParent} + diff --git a/package.json b/package.json index 9aa61624..ad39bfea 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "devDependencies": { "fs-extra": "^8.1.0", "igv": "github:igvteam/igv.js#master", - "igv-widgets": "github:igvteam/igv-widgets#v1.5.8", + "data-modal": "github:igvteam/data-modal#v1.5.0", + "igv-utils": "github:igvteam/igv-utils#v1.5.4", "google-utils": "github:igvteam/google-utils#v1.0.2", "rollup": "^2.28.1", "rollup-plugin-copy": "^3.3.0",