changing the document is now possible
This commit is contained in:
parent
328dfacf47
commit
aecaea3ef1
6 changed files with 276 additions and 63 deletions
|
|
@ -65,6 +65,14 @@
|
||||||
|
|
||||||
<table id="controls">
|
<table id="controls">
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="doc">Document:</label>
|
||||||
|
<select id="doc" size="1">
|
||||||
|
<option value="js">JS Document</option>
|
||||||
|
<option value="html">HTML Document</option>
|
||||||
|
<option value="css">CSS Document</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<label for="mode">Mode:</label>
|
<label for="mode">Mode:</label>
|
||||||
<select id="mode" size="1">
|
<select id="mode" size="1">
|
||||||
|
|
@ -89,11 +97,73 @@
|
||||||
<div id="container">
|
<div id="container">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/editor" id="jstext">function foo(items) {
|
||||||
|
for (var i=0; i<items.length; i++) {
|
||||||
|
alert(items[i] + "juhu";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/editor" id="csstext">.text-layer {
|
||||||
|
font-family: Monaco, "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/editor" id="htmltext"><html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 style="color:red">Juhu Kinners</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript" charset="utf-8">
|
<script type="text/javascript" charset="utf-8">
|
||||||
|
|
||||||
|
var docs = {}
|
||||||
|
docs.js = new ace.TextDocument(document.getElementById("jstext").innerHTML);
|
||||||
|
docs.js.setMode(new ace.mode.JavaScript());
|
||||||
|
docs.css = new ace.TextDocument(document.getElementById("csstext").innerHTML);
|
||||||
|
docs.css.setMode(new ace.mode.Css());
|
||||||
|
docs.html = new ace.TextDocument(document.getElementById("htmltext").innerHTML);
|
||||||
|
docs.html.setMode(new ace.mode.Html());
|
||||||
|
|
||||||
|
var docEl = document.getElementById("doc");
|
||||||
|
|
||||||
|
function onDocChange() {
|
||||||
|
var doc = getDoc();
|
||||||
|
editor.setDocument(doc);
|
||||||
|
|
||||||
|
var mode = doc.getMode();
|
||||||
|
if (mode instanceof ace.mode.JavaScript) {
|
||||||
|
modeEl.value = "javascript"
|
||||||
|
}
|
||||||
|
else if (mode instanceof ace.mode.Css) {
|
||||||
|
modeEl.value = "css"
|
||||||
|
}
|
||||||
|
else if (mode instanceof ace.mode.Html) {
|
||||||
|
modeEl.value = "html"
|
||||||
|
}
|
||||||
|
else if (mode instanceof ace.mode.Xml) {
|
||||||
|
modeEl.value = "xml"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
modeEl.value = "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.focus();
|
||||||
|
}
|
||||||
|
docEl.onchange = onDocChange;
|
||||||
|
|
||||||
|
function getDoc() {
|
||||||
|
return docs[docEl.value];
|
||||||
|
}
|
||||||
|
|
||||||
var modeEl = document.getElementById("mode");
|
var modeEl = document.getElementById("mode");
|
||||||
modeEl.onchange = function() {
|
modeEl.onchange = function() {
|
||||||
editor.setMode(getMode());
|
editor.getDocument().setMode(getMode());
|
||||||
};
|
};
|
||||||
|
|
||||||
var modes = {
|
var modes = {
|
||||||
|
|
@ -117,17 +187,14 @@ selectEl.onchange = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var selectEl = document.getElementById("highlight_active");
|
var activeEl = document.getElementById("highlight_active");
|
||||||
selectEl.onchange = function() {
|
activeEl.onchange = function() {
|
||||||
editor.setHighlightActiveLine(!!selectEl.checked);
|
editor.setHighlightActiveLine(!!activeEl.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
var container = document.getElementById("container");
|
var container = document.getElementById("container");
|
||||||
var editor = new ace.Editor(
|
var editor = new ace.Editor(new ace.VirtualRenderer(container));
|
||||||
new ace.VirtualRenderer(container),
|
onDocChange();
|
||||||
new ace.TextDocument("Juhu Kinners"),
|
|
||||||
getMode()
|
|
||||||
);
|
|
||||||
|
|
||||||
function onResize() {
|
function onResize() {
|
||||||
container.style.width = (document.documentElement.clientWidth - 4) + "px";
|
container.style.width = (document.documentElement.clientWidth - 4) + "px";
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
ace.provide("ace.Editor");
|
ace.provide("ace.Editor");
|
||||||
|
|
||||||
ace.Editor = function(renderer, doc, mode) {
|
ace.Editor = function(renderer, doc) {
|
||||||
var container = renderer.getContainerElement();
|
var container = renderer.getContainerElement();
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
|
|
||||||
this.setMode(mode || new ace.mode.Text());
|
|
||||||
this.setDocument(doc || new ace.TextDocument(""));
|
|
||||||
|
|
||||||
this.textInput = new ace.TextInput(container, this);
|
this.textInput = new ace.TextInput(container, this);
|
||||||
new ace.KeyBinding(container, this);
|
new ace.KeyBinding(container, this);
|
||||||
|
|
||||||
|
|
@ -20,68 +17,59 @@ ace.Editor = function(renderer, doc, mode) {
|
||||||
this._highlightLineMarker = null;
|
this._highlightLineMarker = null;
|
||||||
this._blockScrolling = false;
|
this._blockScrolling = false;
|
||||||
|
|
||||||
this.renderer.draw();
|
this.setDocument(doc || new ace.TextDocument(""));
|
||||||
this.onCursorChange();
|
|
||||||
this.onSelectionChange();
|
|
||||||
|
|
||||||
this._initialized = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
ace.Editor.prototype.setDocument = function(doc) {
|
ace.Editor.prototype.setDocument = function(doc) {
|
||||||
// TODO: document change is not yet supported
|
if (this.doc == doc) return;
|
||||||
|
|
||||||
if (this.doc) {
|
if (this.doc) {
|
||||||
throw new Error("TODO: document change is not yet supported");
|
this.doc.removeEventListener("change", this._onDocumentChange);
|
||||||
|
this.doc.removeEventListener("changeMode", this._onDocumentModeChange);
|
||||||
|
this.doc.removeEventListener("changeTabSize", this._onDocumentChangeTabSize);
|
||||||
|
|
||||||
|
var selection = this.doc.getSelection();
|
||||||
|
this.selection.removeEventListener("changeCursor", this._onCursorChange);
|
||||||
|
this.selection.removeEventListener("changeSelection", this._onSelectionChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.doc = doc;
|
this.doc = doc;
|
||||||
|
|
||||||
doc.addEventListener("change", ace.bind(this.onDocumentChange, this));
|
this._onDocumentChange = ace.bind(this.onDocumentChange, this);
|
||||||
|
doc.addEventListener("change", this._onDocumentChange);
|
||||||
this.renderer.setDocument(doc);
|
this.renderer.setDocument(doc);
|
||||||
|
|
||||||
var self = this;
|
this._onDocumentModeChange = ace.bind(this.onDocumentModeChange, this);
|
||||||
doc.addEventListener("changeTabSize", function() {
|
doc.addEventListener("changeMode", this._onDocumentModeChange);
|
||||||
self.renderer.draw();
|
|
||||||
});
|
this._onDocumentChangeTabSize = ace.bind(this.renderer.draw, this.renderer);
|
||||||
|
doc.addEventListener("changeTabSize", this._onDocumentChangeTabSize);
|
||||||
|
|
||||||
this.selection = doc.getSelection();
|
this.selection = doc.getSelection();
|
||||||
|
|
||||||
var onCursorChange = ace.bind(this.onCursorChange, this);
|
this._onCursorChange = ace.bind(this.onCursorChange, this);
|
||||||
this.selection.addEventListener("changeCursor", onCursorChange);
|
this.selection.addEventListener("changeCursor", this._onCursorChange);
|
||||||
|
|
||||||
var onSelectionChange = ace.bind(this.onSelectionChange, this);
|
|
||||||
this.selection.addEventListener("changeSelection", onSelectionChange);
|
|
||||||
|
|
||||||
|
this._onSelectionChange = ace.bind(this.onSelectionChange, this);
|
||||||
|
this.selection.addEventListener("changeSelection", this._onSelectionChange);
|
||||||
|
|
||||||
|
this.onDocumentModeChange();
|
||||||
this.bgTokenizer.setLines(this.doc.lines);
|
this.bgTokenizer.setLines(this.doc.lines);
|
||||||
|
|
||||||
|
this.renderer.draw();
|
||||||
|
this.onCursorChange();
|
||||||
|
this.onSelectionChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
ace.Editor.prototype.getDocument = function() {
|
||||||
|
return this.doc;
|
||||||
};
|
};
|
||||||
|
|
||||||
ace.Editor.prototype.getSelection = function() {
|
ace.Editor.prototype.getSelection = function() {
|
||||||
return this.selection;
|
return this.selection;
|
||||||
};
|
};
|
||||||
|
|
||||||
ace.Editor.prototype.setMode = function(mode) {
|
|
||||||
if (this.mode == mode) return;
|
|
||||||
|
|
||||||
this.mode = mode;
|
|
||||||
var tokenizer = mode.getTokenizer();
|
|
||||||
|
|
||||||
if (!this.bgTokenizer) {
|
|
||||||
var onUpdate = ace.bind(this.onTokenizerUpdate, this);
|
|
||||||
this.bgTokenizer = new ace.BackgroundTokenizer(tokenizer);
|
|
||||||
this.bgTokenizer.addEventListener("update", onUpdate);
|
|
||||||
} else {
|
|
||||||
this.bgTokenizer.setTokenizer(tokenizer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderer.setTokenizer(this.bgTokenizer);
|
|
||||||
|
|
||||||
if (this._initialized) {
|
|
||||||
this.renderer.draw();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
ace.Editor.prototype.resize = function() {
|
ace.Editor.prototype.resize = function() {
|
||||||
this.renderer.onResize();
|
this.renderer.onResize();
|
||||||
};
|
};
|
||||||
|
|
@ -116,6 +104,9 @@ ace.Editor.prototype._highlightBrackets = function() {
|
||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ace.Editor.prototype.focus = function() {
|
||||||
|
this.textInput.focus();
|
||||||
|
};
|
||||||
|
|
||||||
ace.Editor.prototype.onFocus = function() {
|
ace.Editor.prototype.onFocus = function() {
|
||||||
this.renderer.showCursor();
|
this.renderer.showCursor();
|
||||||
|
|
@ -185,8 +176,27 @@ ace.Editor.prototype.onSelectionChange = function() {
|
||||||
this.onCursorChange();
|
this.onCursorChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ace.Editor.prototype.onDocumentModeChange = function() {
|
||||||
|
var mode = this.doc.getMode();
|
||||||
|
|
||||||
|
this.mode = mode;
|
||||||
|
var tokenizer = mode.getTokenizer();
|
||||||
|
|
||||||
|
if (!this.bgTokenizer) {
|
||||||
|
var onUpdate = ace.bind(this.onTokenizerUpdate, this);
|
||||||
|
this.bgTokenizer = new ace.BackgroundTokenizer(tokenizer);
|
||||||
|
this.bgTokenizer.addEventListener("update", onUpdate);
|
||||||
|
} else {
|
||||||
|
this.bgTokenizer.setTokenizer(tokenizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer.setTokenizer(this.bgTokenizer);
|
||||||
|
this.renderer.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
ace.Editor.prototype.onMouseDown = function(e) {
|
ace.Editor.prototype.onMouseDown = function(e) {
|
||||||
this.textInput.focus();
|
this.focus();
|
||||||
|
|
||||||
var pageX = ace.getDocumentX(e);
|
var pageX = ace.getDocumentX(e);
|
||||||
var pageY = ace.getDocumentY(e);
|
var pageY = ace.getDocumentY(e);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ ace.MEventEmitter.$initEvents = function() {
|
||||||
|
|
||||||
ace.MEventEmitter.$dispatchEvent = function(eventName, e) {
|
ace.MEventEmitter.$dispatchEvent = function(eventName, e) {
|
||||||
var listeners = this._eventRegistry[eventName];
|
var listeners = this._eventRegistry[eventName];
|
||||||
if (!listeners) return;
|
if (!listeners || !listeners.length) return;
|
||||||
|
|
||||||
var e = e || {};
|
var e = e || {};
|
||||||
e.type = eventName;
|
e.type = eventName;
|
||||||
|
|
@ -25,3 +25,14 @@ ace.MEventEmitter.addEventListener = function(eventName, callback) {
|
||||||
listeners.push(callback);
|
listeners.push(callback);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ace.MEventEmitter.removeEventListener = function(eventName, callback) {
|
||||||
|
var listeners = this._eventRegistry[eventName];
|
||||||
|
if (!listeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var index = ace.arrayIndexOf(listeners, callback);
|
||||||
|
if (index !== -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
ace.provide("ace.TextDocument");
|
ace.provide("ace.TextDocument");
|
||||||
|
|
||||||
ace.TextDocument = function(text) {
|
ace.TextDocument = function(text, mode) {
|
||||||
|
this.$initEvents();
|
||||||
|
|
||||||
this.lines = this._split(text);
|
this.lines = this._split(text);
|
||||||
this.modified = true;
|
this.modified = true;
|
||||||
this.selection = new ace.Selection(this);
|
this.selection = new ace.Selection(this);
|
||||||
|
|
||||||
this.listeners = [];
|
this.listeners = [];
|
||||||
|
if (mode) {
|
||||||
this.$initEvents();
|
this.setMode(mode);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
ace.mixin(ace.TextDocument.prototype, ace.MEventEmitter);
|
|
||||||
|
|
||||||
|
ace.mixin(ace.TextDocument.prototype, ace.MEventEmitter);
|
||||||
|
|
||||||
ace.TextDocument.prototype._split = function(text) {
|
ace.TextDocument.prototype._split = function(text) {
|
||||||
return text.split(/\r\n|\r|\n/);
|
return text.split(/\r\n|\r|\n/);
|
||||||
|
|
@ -62,6 +65,21 @@ ace.TextDocument.prototype.getTabSize = function() {
|
||||||
return this._tabSize;
|
return this._tabSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ace.TextDocument.prototype._mode = null;
|
||||||
|
ace.TextDocument.prototype.setMode = function(mode) {
|
||||||
|
if (this._mode === mode) return;
|
||||||
|
|
||||||
|
this._mode = mode;
|
||||||
|
this.$dispatchEvent("changeMode");
|
||||||
|
};
|
||||||
|
|
||||||
|
ace.TextDocument.prototype.getMode = function() {
|
||||||
|
if (!this._mode) {
|
||||||
|
this._mode = new ace.mode.Text();
|
||||||
|
}
|
||||||
|
return this._mode;
|
||||||
|
};
|
||||||
|
|
||||||
ace.TextDocument.prototype.getWidth = function() {
|
ace.TextDocument.prototype.getWidth = function() {
|
||||||
if (this.modified) {
|
if (this.modified) {
|
||||||
this.modified = false;
|
this.modified = false;
|
||||||
|
|
|
||||||
107
test/ChangeDocumentTest.js
Normal file
107
test/ChangeDocumentTest.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
var ChangeDocumentTest = new TestCase("ChangeDocumentTest", {
|
||||||
|
|
||||||
|
setUp : function() {
|
||||||
|
this.doc1 = new ace.TextDocument(["abc", "def"].join("\n"));
|
||||||
|
this.doc2 = new ace.TextDocument(["ghi", "jkl"].join("\n"));
|
||||||
|
this.editor = new ace.Editor(new MockRenderer());
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: change document" : function() {
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
assertEquals(this.doc1, this.editor.getDocument());
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
assertEquals(this.doc2, this.editor.getDocument());
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: only changes to the new document should have effect" : function() {
|
||||||
|
var called = false;
|
||||||
|
this.editor.onDocumentChange = function() {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
|
||||||
|
this.doc1.duplicateLines(0, 0);
|
||||||
|
assertFalse(called);
|
||||||
|
|
||||||
|
this.doc2.duplicateLines(0, 0);
|
||||||
|
assertTrue(called);
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: should use cursor of new document" : function() {
|
||||||
|
this.doc1.getSelection().moveCursorTo(0, 1);
|
||||||
|
this.doc2.getSelection().moveCursorTo(1, 0);
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
assertPosition(0, 1, this.editor.getCursorPosition());
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
assertPosition(1, 0, this.editor.getCursorPosition());
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: only changing the cursor of the new doc should not have an effect" : function() {
|
||||||
|
this.editor.onCursorChange = function() {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
assertPosition(0, 0, this.editor.getCursorPosition());
|
||||||
|
|
||||||
|
var called = false;
|
||||||
|
this.doc1.getSelection().moveCursorTo(0, 1);
|
||||||
|
assertPosition(0, 0, this.editor.getCursorPosition());
|
||||||
|
assertFalse(called);
|
||||||
|
|
||||||
|
this.doc2.getSelection().moveCursorTo(1, 1);
|
||||||
|
assertPosition(1, 1, this.editor.getCursorPosition());
|
||||||
|
assertTrue(called);
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: should use selection of new document" : function() {
|
||||||
|
this.doc1.getSelection().selectTo(0, 1);
|
||||||
|
this.doc2.getSelection().selectTo(1, 0);
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
assertPosition(0, 1, this.editor.getSelection().getSelectionLead());
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
assertPosition(1, 0, this.editor.getSelection().getSelectionLead());
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: only changing the selection of the new doc should not have an effect" : function() {
|
||||||
|
this.editor.onSelectionChange = function() {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
assertPosition(0, 0, this.editor.getSelection().getSelectionLead());
|
||||||
|
|
||||||
|
var called = false;
|
||||||
|
this.doc1.getSelection().selectTo(0, 1);
|
||||||
|
assertPosition(0, 0, this.editor.getSelection().getSelectionLead());
|
||||||
|
assertFalse(called);
|
||||||
|
|
||||||
|
this.doc2.getSelection().selectTo(1, 1);
|
||||||
|
assertPosition(1, 1, this.editor.getSelection().getSelectionLead());
|
||||||
|
assertTrue(called);
|
||||||
|
},
|
||||||
|
|
||||||
|
"test: should use mode of new document" : function() {
|
||||||
|
this.editor.onDocumentModeChange = function() {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
this.editor.setDocument(this.doc1);
|
||||||
|
this.editor.setDocument(this.doc2);
|
||||||
|
|
||||||
|
var called = false;
|
||||||
|
this.doc1.setMode(new ace.mode.Text());
|
||||||
|
assertFalse(called);
|
||||||
|
|
||||||
|
this.doc2.setMode(new ace.mode.JavaScript());
|
||||||
|
assertTrue(called);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -118,8 +118,8 @@ var TextEditTest = TestCase("TextEditTest",
|
||||||
},
|
},
|
||||||
|
|
||||||
"test: comment lines should perserve selection" : function() {
|
"test: comment lines should perserve selection" : function() {
|
||||||
var doc = new ace.TextDocument([" abc", "cde"].join("\n"));
|
var doc = new ace.TextDocument([" abc", "cde"].join("\n"), new ace.mode.JavaScript());
|
||||||
var editor = new ace.Editor(new MockRenderer(), doc, new ace.mode.JavaScript());
|
var editor = new ace.Editor(new MockRenderer(), doc);
|
||||||
|
|
||||||
editor.moveCursorTo(0, 2);
|
editor.moveCursorTo(0, 2);
|
||||||
editor.getSelection().selectDown();
|
editor.getSelection().selectDown();
|
||||||
|
|
@ -134,8 +134,8 @@ var TextEditTest = TestCase("TextEditTest",
|
||||||
},
|
},
|
||||||
|
|
||||||
"test: uncomment lines should perserve selection" : function() {
|
"test: uncomment lines should perserve selection" : function() {
|
||||||
var doc = new ace.TextDocument(["// abc", "//cde"].join("\n"));
|
var doc = new ace.TextDocument(["// abc", "//cde"].join("\n"), new ace.mode.JavaScript());
|
||||||
var editor = new ace.Editor(new MockRenderer(), doc, new ace.mode.JavaScript());
|
var editor = new ace.Editor(new MockRenderer(), doc);
|
||||||
|
|
||||||
editor.moveCursorTo(0, 1);
|
editor.moveCursorTo(0, 1);
|
||||||
editor.getSelection().selectDown();
|
editor.getSelection().selectDown();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue