mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-23 08:16:17 -04:00
statusbar popup keyboard navigation
This commit is contained in:
parent
85a3510454
commit
6fcf103760
3 changed files with 135 additions and 126 deletions
|
@ -478,6 +478,11 @@
|
|||
background-color: #ddd
|
||||
}
|
||||
|
||||
/* Change color of dropup links on focus */
|
||||
.cm-status-bar-select-content a:focus {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
/* Change the background color of the dropup button when the dropup content is shown */
|
||||
.cm-status-bar-select:hover .cm-status-bar-select-btn {
|
||||
background-color: #f1f1f1;
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
*/
|
||||
|
||||
import {showPanel} from "@codemirror/view";
|
||||
import {
|
||||
CHR_ENC_SIMPLE_LOOKUP,
|
||||
CHR_ENC_SIMPLE_REVERSE_LOOKUP,
|
||||
} from "../../core/lib/ChrEnc.mjs";
|
||||
import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
|
||||
|
||||
/**
|
||||
* A Status bar extension for CodeMirror
|
||||
|
@ -44,10 +41,7 @@ class StatusBarPanel {
|
|||
|
||||
dom.className = "cm-status-bar";
|
||||
dom.setAttribute("data-help-title", `${this.label} status bar`);
|
||||
dom.setAttribute(
|
||||
"data-help",
|
||||
`This status bar provides information about data in the ${this.label}. Help topics are available for each of the components by activating help when hovering over them.`
|
||||
);
|
||||
dom.setAttribute("data-help", `This status bar provides information about data in the ${this.label}. Help topics are available for each of the components by activating help when hovering over them.`);
|
||||
lhs.innerHTML = this.constructLHS();
|
||||
rhs.innerHTML = this.constructRHS();
|
||||
|
||||
|
@ -58,25 +52,27 @@ class StatusBarPanel {
|
|||
const eventHandler = this.showDropUp.bind(this);
|
||||
|
||||
dom.querySelectorAll(".cm-status-bar-select-btn").forEach((el) => {
|
||||
el.addEventListener("click", eventHandler, false),
|
||||
el.addEventListener("click", eventHandler, false);
|
||||
});
|
||||
dom.querySelectorAll(".cm-status-bar-select-btn").forEach((el) => {
|
||||
el.addEventListener("keydown", eventHandler, false);
|
||||
});
|
||||
dom.querySelector(".eol-select").addEventListener(
|
||||
"click",
|
||||
this.eolSelectClick.bind(this),
|
||||
false
|
||||
);
|
||||
dom.querySelector(".chr-enc-select").addEventListener(
|
||||
"click",
|
||||
this.chrEncSelectClick.bind(this),
|
||||
false
|
||||
);
|
||||
dom.querySelector(".cm-status-bar-filter-input").addEventListener(
|
||||
"keyup",
|
||||
this.chrEncFilter.bind(this),
|
||||
false
|
||||
|
||||
const selectContent = dom.querySelectorAll(
|
||||
".cm-status-bar-select-content"
|
||||
);
|
||||
|
||||
selectContent.forEach((el) => {
|
||||
const aTags = el.getElementsByTagName("a");
|
||||
|
||||
for (let i = 0; i < aTags.length; i++) {
|
||||
aTags[i].addEventListener("keydown", arrowNav, false);
|
||||
}
|
||||
});
|
||||
dom.querySelector(".eol-select").addEventListener("click", this.eolSelectClick.bind(this), false);
|
||||
dom.querySelector(".chr-enc-select").addEventListener("click", this.chrEncSelectClick.bind(this), false);
|
||||
dom.querySelector(".cm-status-bar-filter-input").addEventListener("keyup", this.chrEncFilter.bind(this), false);
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
|
@ -117,14 +113,14 @@ class StatusBarPanel {
|
|||
e.preventDefault();
|
||||
|
||||
const eolLookup = {
|
||||
LF: "\u000a",
|
||||
VT: "\u000b",
|
||||
FF: "\u000c",
|
||||
CR: "\u000d",
|
||||
CRLF: "\u000d\u000a",
|
||||
NEL: "\u0085",
|
||||
LS: "\u2028",
|
||||
PS: "\u2029",
|
||||
"LF": "\u000a",
|
||||
"VT": "\u000b",
|
||||
"FF": "\u000c",
|
||||
"CR": "\u000d",
|
||||
"CRLF": "\u000d\u000a",
|
||||
"NEL": "\u0085",
|
||||
"LS": "\u2028",
|
||||
"PS": "\u2029"
|
||||
};
|
||||
const eolval = eolLookup[e.target.getAttribute("data-val")];
|
||||
|
||||
|
@ -198,9 +194,9 @@ class StatusBarPanel {
|
|||
* @param {boolean} selectionSet
|
||||
*/
|
||||
updateSelection(state, selectionSet) {
|
||||
const selLen = state?.selection?.main
|
||||
? state.selection.main.to - state.selection.main.from
|
||||
: 0;
|
||||
const selLen = state?.selection?.main ?
|
||||
state.selection.main.to - state.selection.main.from :
|
||||
0;
|
||||
|
||||
const selInfo = this.dom.querySelector(".sel-info"),
|
||||
curOffsetInfo = this.dom.querySelector(".cur-offset-info");
|
||||
|
@ -218,12 +214,11 @@ class StatusBarPanel {
|
|||
if (state.lineBreak.length !== 1) {
|
||||
const fromLine = state.doc.lineAt(from).number;
|
||||
const toLine = state.doc.lineAt(to).number;
|
||||
from += state.lineBreak.length * fromLine - fromLine - 1;
|
||||
to += state.lineBreak.length * toLine - toLine - 1;
|
||||
from += (state.lineBreak.length * fromLine) - fromLine - 1;
|
||||
to += (state.lineBreak.length * toLine) - toLine - 1;
|
||||
}
|
||||
|
||||
if (selLen > 0) {
|
||||
// Range
|
||||
if (selLen > 0) { // Range
|
||||
const start = this.dom.querySelector(".sel-start-value"),
|
||||
end = this.dom.querySelector(".sel-end-value"),
|
||||
length = this.dom.querySelector(".sel-length-value");
|
||||
|
@ -233,8 +228,7 @@ class StatusBarPanel {
|
|||
start.textContent = from;
|
||||
end.textContent = to;
|
||||
length.textContent = to - from;
|
||||
} else {
|
||||
// Position
|
||||
} else { // Position
|
||||
const offset = this.dom.querySelector(".cur-offset-value");
|
||||
|
||||
selInfo.style.display = "none";
|
||||
|
@ -258,7 +252,7 @@ class StatusBarPanel {
|
|||
"\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"],
|
||||
"\u0085": ["NEL", "Next Line"],
|
||||
"\u2028": ["LS", "Line Separator"],
|
||||
"\u2029": ["PS", "Paragraph Separator"],
|
||||
"\u2029": ["PS", "Paragraph Separator"]
|
||||
};
|
||||
|
||||
const val = this.dom.querySelector(".eol-value");
|
||||
|
@ -266,10 +260,7 @@ class StatusBarPanel {
|
|||
const eolName = eolLookup[state.lineBreak];
|
||||
val.textContent = eolName[0];
|
||||
button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`);
|
||||
button.setAttribute(
|
||||
"data-original-title",
|
||||
`End of line sequence:<br>${eolName[1]}`
|
||||
);
|
||||
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName[1]}`);
|
||||
this.eolVal = state.lineBreak;
|
||||
}
|
||||
|
||||
|
@ -280,21 +271,13 @@ class StatusBarPanel {
|
|||
const chrEncVal = this.chrEncGetter();
|
||||
if (chrEncVal === this.chrEncVal) return;
|
||||
|
||||
const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal]
|
||||
? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal]
|
||||
: "Raw Bytes";
|
||||
const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes";
|
||||
|
||||
const val = this.dom.querySelector(".chr-enc-value");
|
||||
const button = val.closest(".cm-status-bar-select-btn");
|
||||
val.textContent = name;
|
||||
button.setAttribute(
|
||||
"title",
|
||||
`${this.label} character encoding:<br>${name}`
|
||||
);
|
||||
button.setAttribute(
|
||||
"data-original-title",
|
||||
`${this.label} character encoding:<br>${name}`
|
||||
);
|
||||
button.setAttribute("title", `${this.label} character encoding:<br>${name}`);
|
||||
button.setAttribute("data-original-title", `${this.label} character encoding:<br>${name}`);
|
||||
this.chrEncVal = chrEncVal;
|
||||
}
|
||||
|
||||
|
@ -311,9 +294,7 @@ class StatusBarPanel {
|
|||
bakingTimeInfo.style.display = "inline-block";
|
||||
bakingTime.textContent = this.timing.duration(this.tabNumGetter());
|
||||
|
||||
const info = this.timing
|
||||
.printStages(this.tabNumGetter())
|
||||
.replace(/\n/g, "<br>");
|
||||
const info = this.timing.printStages(this.tabNumGetter()).replace(/\n/g, "<br>");
|
||||
bakingTimeInfo.setAttribute("data-original-title", info);
|
||||
} else {
|
||||
bakingTimeInfo.style.display = "none";
|
||||
|
@ -326,11 +307,11 @@ class StatusBarPanel {
|
|||
*/
|
||||
updateSizing(view) {
|
||||
const viewHeight = view.contentDOM.parentNode.clientHeight;
|
||||
this.dom
|
||||
.querySelectorAll(".cm-status-bar-select-scroll")
|
||||
.forEach((el) => {
|
||||
el.style.maxHeight = viewHeight - 50 + "px";
|
||||
});
|
||||
this.dom.querySelectorAll(".cm-status-bar-select-scroll").forEach(
|
||||
el => {
|
||||
el.style.maxHeight = (viewHeight - 50) + "px";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -341,27 +322,19 @@ class StatusBarPanel {
|
|||
|
||||
if (this.htmlOutput?.html === "") {
|
||||
// Enable all controls
|
||||
this.dom.querySelectorAll(".disabled").forEach((el) => {
|
||||
this.dom.querySelectorAll(".disabled").forEach(el => {
|
||||
el.classList.remove("disabled");
|
||||
});
|
||||
} else {
|
||||
// Disable chrenc, length, selection etc.
|
||||
this.dom
|
||||
.querySelectorAll(".cm-status-bar-select-btn")
|
||||
.forEach((el) => {
|
||||
this.dom.querySelectorAll(".cm-status-bar-select-btn").forEach(el => {
|
||||
el.classList.add("disabled");
|
||||
});
|
||||
|
||||
this.dom
|
||||
.querySelector(".stats-length-value")
|
||||
.parentNode.classList.add("disabled");
|
||||
this.dom
|
||||
.querySelector(".stats-lines-value")
|
||||
.parentNode.classList.add("disabled");
|
||||
this.dom.querySelector(".stats-length-value").parentNode.classList.add("disabled");
|
||||
this.dom.querySelector(".stats-lines-value").parentNode.classList.add("disabled");
|
||||
this.dom.querySelector(".sel-info").classList.add("disabled");
|
||||
this.dom
|
||||
.querySelector(".cur-offset-info")
|
||||
.classList.add("disabled");
|
||||
this.dom.querySelector(".cur-offset-info").classList.add("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,25 +371,18 @@ class StatusBarPanel {
|
|||
* @returns {string}
|
||||
*/
|
||||
constructRHS() {
|
||||
const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP)
|
||||
.map(
|
||||
(name) =>
|
||||
const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP).map(name =>
|
||||
`<a href="#" draggable="false" data-val="${CHR_ENC_SIMPLE_LOOKUP[name]}">${name}</a>`
|
||||
)
|
||||
.join("");
|
||||
).join("");
|
||||
|
||||
let chrEncHelpText = "",
|
||||
eolHelpText = "";
|
||||
if (this.label === "Input") {
|
||||
chrEncHelpText =
|
||||
"The input character encoding defines how the input text is encoded into bytes which are then processed by the Recipe.<br><br>The 'Raw bytes' option attempts to treat the input as individual bytes in the range 0-255. If it detects any characters with Unicode values above 255, it will treat the entire input as UTF-8. 'Raw bytes' is usually the best option if you are inputting binary data, such as a file.";
|
||||
eolHelpText =
|
||||
"The End of Line Sequence defines which bytes are considered EOL terminators. Pressing the return key will enter this value into the input and create a new line.<br><br>Changing the EOL sequence will not modify any existing data in the input but may change how previously entered line breaks are displayed. Lines added while a different EOL terminator was set may not now result in a new line, but may be displayed as control characters instead.";
|
||||
chrEncHelpText = "The input character encoding defines how the input text is encoded into bytes which are then processed by the Recipe.<br><br>The 'Raw bytes' option attempts to treat the input as individual bytes in the range 0-255. If it detects any characters with Unicode values above 255, it will treat the entire input as UTF-8. 'Raw bytes' is usually the best option if you are inputting binary data, such as a file.";
|
||||
eolHelpText = "The End of Line Sequence defines which bytes are considered EOL terminators. Pressing the return key will enter this value into the input and create a new line.<br><br>Changing the EOL sequence will not modify any existing data in the input but may change how previously entered line breaks are displayed. Lines added while a different EOL terminator was set may not now result in a new line, but may be displayed as control characters instead.";
|
||||
} else {
|
||||
chrEncHelpText =
|
||||
"The output character encoding defines how the output bytes are decoded into text which can be displayed to you.<br><br>The 'Raw bytes' option treats the output data as individual bytes in the range 0-255.";
|
||||
eolHelpText =
|
||||
"The End of Line Sequence defines which bytes are considered EOL terminators.<br><br>Changing this value will not modify the value of the output, but may change how certain bytes are displayed and whether they result in a new line being created.";
|
||||
chrEncHelpText = "The output character encoding defines how the output bytes are decoded into text which can be displayed to you.<br><br>The 'Raw bytes' option treats the output data as individual bytes in the range 0-255.";
|
||||
eolHelpText = "The End of Line Sequence defines which bytes are considered EOL terminators.<br><br>Changing this value will not modify the value of the output, but may change how certain bytes are displayed and whether they result in a new line being created.";
|
||||
}
|
||||
|
||||
return `
|
||||
|
@ -431,7 +397,7 @@ class StatusBarPanel {
|
|||
</span>
|
||||
<div class="cm-status-bar-select-content">
|
||||
<div class="cm-status-bar-select-scroll no-select">
|
||||
<a href="#" draggable="false" data-val="0">Raw Bytes</a>
|
||||
<a href="#" draggable="false" data-val="0" tabindex="0">Raw Bytes</a>
|
||||
${chrEncOptions}
|
||||
</div>
|
||||
<div class="input-group cm-status-bar-filter-search">
|
||||
|
@ -449,15 +415,15 @@ class StatusBarPanel {
|
|||
<span class="cm-status-bar-select-btn" data-toggle="tooltip" tabindex="0" data-html="true" data-placement="left" title="End of line sequence">
|
||||
<i class="material-icons">keyboard_return</i> <span class="eol-value"></span>
|
||||
</span>
|
||||
<div class="cm-status-bar-select-content no-select">
|
||||
<a href="#" draggable="false" data-val="LF">Line Feed, U+000A</a>
|
||||
<a href="#" draggable="false" data-val="VT">Vertical Tab, U+000B</a>
|
||||
<a href="#" draggable="false" data-val="FF">Form Feed, U+000C</a>
|
||||
<a href="#" draggable="false" data-val="CR">Carriage Return, U+000D</a>
|
||||
<a href="#" draggable="false" data-val="CRLF">CR+LF, U+000D U+000A</a>
|
||||
<!-- <a href="#" draggable="false" data-val="NL">Next Line, U+0085</a> This causes problems. -->
|
||||
<a href="#" draggable="false" data-val="LS">Line Separator, U+2028</a>
|
||||
<a href="#" draggable="false" data-val="PS">Paragraph Separator, U+2029</a>
|
||||
<div class="cm-status-bar-select-content no-select" tabindex="0">
|
||||
<a href="#" draggable="false" data-val="LF" tabindex="0">Line Feed, U+000A</a>
|
||||
<a href="#" draggable="false" data-val="VT" tabindex="0">Vertical Tab, U+000B</a>
|
||||
<a href="#" draggable="false" data-val="FF" tabindex="0">Form Feed, U+000C</a>
|
||||
<a href="#" draggable="false" data-val="CR" tabindex="0">Carriage Return, U+000D</a>
|
||||
<a href="#" draggable="false" data-val="CRLF" tabindex="0">CR+LF, U+000D U+000A</a>
|
||||
<!-- <a href="#" draggable="false" data-val="NL" tabindex="0">Next Line, U+0085</a> This causes problems. -->
|
||||
<a href="#" draggable="false" data-val="LS" tabindex="0">Line Separator, U+2028</a>
|
||||
<a href="#" draggable="false" data-val="PS" tabindex="0">Paragraph Separator, U+2029</a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
@ -476,7 +442,7 @@ function hideOnClickOutside(element, instantiatingEvent) {
|
|||
* Closes element if click is outside it.
|
||||
* @param {Event} event
|
||||
*/
|
||||
const outsideClickListener = (event) => {
|
||||
const outsideClickListener = event => {
|
||||
// Don't trigger if we're clicking inside the element, or if the element
|
||||
// is not visible, or if this is the same click event that opened it.
|
||||
if (
|
||||
|
@ -509,14 +475,30 @@ function hideOnMoveFocus(element, instantiatingEvent) {
|
|||
* Closes element if key press is outside it.
|
||||
* @param {Event} event
|
||||
*/
|
||||
const outsideClickListener = (event) => {
|
||||
const outsidePressListener = (event) => {
|
||||
// Don't trigger if we're pressing keys while inside the element, or if the element
|
||||
// is not visible, or if this is the same click event that opened it.
|
||||
if (
|
||||
!element.contains(event.target) &&
|
||||
event.timeStamp !== instantiatingEvent.timeStamp &&
|
||||
event.key !== "ArrowUp"
|
||||
) {
|
||||
hideElement(element);
|
||||
} else if (
|
||||
event.key === "Escape" &&
|
||||
event.timeStamp !== instantiatingEvent.timeStamp
|
||||
) {
|
||||
hideElement(element);
|
||||
} else if (
|
||||
event.key === "ArrowUp" ||
|
||||
(event.key === "ArrowDown" &&
|
||||
event.timeStamp !== instantiatingEvent.timeStamp)
|
||||
) {
|
||||
const menuItems = element.getElementsByTagName("a");
|
||||
menuItems[0].focus();
|
||||
|
||||
console.log("ev target:", event.target);
|
||||
console.log("element", element);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -526,7 +508,7 @@ function hideOnMoveFocus(element, instantiatingEvent) {
|
|||
element
|
||||
)
|
||||
) {
|
||||
elementsWithKeyDownListeners[element] = outsideClickListener;
|
||||
elementsWithKeyDownListeners[element] = outsidePressListener;
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
elementsWithKeyDownListeners[element],
|
||||
|
@ -535,6 +517,38 @@ function hideOnMoveFocus(element, instantiatingEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for menu item keydown events
|
||||
* Moves focus to next/previous element based on arrow direction.
|
||||
* @param {Event} event
|
||||
*/
|
||||
const arrowNav = (event) => {
|
||||
const currentElement = event.target;
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const nextElement = currentElement.nextElementSibling;
|
||||
if (nextElement === null) {
|
||||
currentElement.parentElement.firstElementChild.focus();
|
||||
} else {
|
||||
nextElement.focus();
|
||||
}
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const prevElement = currentElement.previousElementSibling;
|
||||
if (prevElement === null) {
|
||||
currentElement.parentElement.lastElementChild.focus();
|
||||
} else {
|
||||
prevElement.focus();
|
||||
}
|
||||
} else if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
currentElement.parentElement.closest(".cm-status-bar-select-content").previousElementSibling.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the specified element and removes the click or keydown listener for it
|
||||
* @param {Element} element
|
||||
|
|
|
@ -152,9 +152,7 @@ class ControlsWaiter {
|
|||
|
||||
const params = [
|
||||
includeRecipe ? ["recipe", recipeStr] : undefined,
|
||||
includeInput && input.length
|
||||
? ["input", Utils.escapeHtml(input)]
|
||||
: undefined,
|
||||
includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined,
|
||||
inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined,
|
||||
outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined,
|
||||
inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined,
|
||||
|
@ -255,9 +253,7 @@ class ControlsWaiter {
|
|||
return;
|
||||
}
|
||||
|
||||
const savedRecipes = localStorage.savedRecipes
|
||||
? JSON.parse(localStorage.savedRecipes)
|
||||
: [];
|
||||
const savedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
|
||||
let recipeId = localStorage.recipeId || 0;
|
||||
|
||||
savedRecipes.push({
|
||||
|
@ -287,9 +283,7 @@ class ControlsWaiter {
|
|||
}
|
||||
|
||||
// Add recipes to select
|
||||
const savedRecipes = localStorage.savedRecipes
|
||||
? JSON.parse(localStorage.savedRecipes)
|
||||
: [];
|
||||
const savedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
|
||||
|
||||
for (i = 0; i < savedRecipes.length; i++) {
|
||||
const opt = document.createElement("option");
|
||||
|
@ -316,9 +310,7 @@ class ControlsWaiter {
|
|||
if (!this.app.isLocalStorageAvailable()) return false;
|
||||
|
||||
const id = parseInt(document.getElementById("load-name").value, 10);
|
||||
const rawSavedRecipes = localStorage.savedRecipes
|
||||
? JSON.parse(localStorage.savedRecipes)
|
||||
: [];
|
||||
const rawSavedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
|
||||
|
||||
const savedRecipes = rawSavedRecipes.filter((r) => r.id !== id);
|
||||
|
||||
|
@ -333,9 +325,7 @@ class ControlsWaiter {
|
|||
if (!this.app.isLocalStorageAvailable()) return false;
|
||||
|
||||
const el = e.target;
|
||||
const savedRecipes = localStorage.savedRecipes
|
||||
? JSON.parse(localStorage.savedRecipes)
|
||||
: [];
|
||||
const savedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
|
||||
const id = parseInt(el.value, 10);
|
||||
|
||||
const recipe = savedRecipes.find((r) => r.id === id);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue