Implement history navigation WIP.

This commit is contained in:
Richard Lewis 2020-04-13 11:06:54 +03:00
parent 76d736199b
commit fcf3f7caac
3 changed files with 151 additions and 37 deletions

View file

@ -51,7 +51,9 @@
<WysiwygInput
ref="wysiwyg"
:placeholder="getInputPlaceholder(channel)"
@change="onChange"
@submit="onSubmit"
@navigate="onNavigate"
/>
<span
id="format-tooltip"
@ -257,10 +259,10 @@ export default {
upload.abort();
},
methods: {
setPendingMessage(e) {
this.channel.pendingMessage = e.target.value;
setPendingMessage(text) {
console.log("setPendingMessage", text);
this.channel.pendingMessage = text;
this.channel.inputHistoryPosition = 0;
this.setInputSize();
},
getInputPlaceholder(channel) {
if (channel.type === "channel" || channel.type === "query") {
@ -269,6 +271,11 @@ export default {
return "";
},
onChange() {
console.log("onChange");
this.setPendingMessage(this.$refs.wysiwyg.getHtmlContent());
console.log(this.channel.pendingMessage);
},
onSubmit(e) {
e.preventDefault();
@ -277,15 +284,54 @@ export default {
}
const target = this.channel.id;
// const content = this.$refs.wysiwyg.getHtmlContent(); TODO: use this for input history
const text = this.$refs.wysiwyg.getHtmlContent(); // TODO: use this for input history
const lines = this.$refs.wysiwyg.getIrcLines();
const message = lines.join("\n");
if (!message) {
return false;
}
this.channel.inputHistoryPosition = 0;
this.channel.pendingMessage = "";
// this.$refs.input.value = ""; TODO: check if necessary
//this.setInputSize();
// Store new message in history if last message isn't already equal
if (this.channel.inputHistory[1] !== text) {
this.channel.inputHistory.splice(1, 0, text);
}
// Limit input history to a 100 entries
if (this.channel.inputHistory.length > 100) {
this.channel.inputHistory.pop();
}
this.$refs.wysiwyg.clear();
this.$refs.wysiwyg.focus();
const message = lines.join("\n");
console.log(`SENDING: "${message}"`);
socket.emit("input", {target, text: message});
},
onNavigate(data) {
console.log("onNavigate", data);
const {channel} = this;
if (channel.inputHistoryPosition === 0) {
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
}
if (data.direction === "up") {
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
channel.inputHistoryPosition++;
}
} else if (channel.inputHistoryPosition > 0) {
channel.inputHistoryPosition--;
}
channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition];
this.$refs.wysiwyg.setHtmlContent(channel.pendingMessage);
},
/*
onSubmit() {

View file

@ -1,6 +1,7 @@
<template>
<div id="input" ref="container" class="wysiwyg-container">
<div ref="indicator" class="indicator"></div>
<div ref="selectionIndicator" class="selectionIndicator"></div>
<div ref="textboxIndicator" class="textboxIndicator"></div>
<IrcColorPicker ref="colorpicker" />
<div
id="wysiwyg-input"
@ -12,6 +13,8 @@
:aria-label="placeholder"
@focus="onFocus"
@blur="onBlur"
@keydown.exact.up="navigate('up')"
@keydown.exact.down="navigate('down')"
></div>
</div>
</template>
@ -44,7 +47,10 @@
display: block;
content: attr(data-placeholder);
}
.wysiwyg-container .wysiwyg-input::after {
content: "";
display: inline-block;
}
.wysiwyg-container .wysiwyg-input sub {
font-size: inherit;
font-family: monospace;
@ -52,12 +58,18 @@
}
/* TODO: remove this */
.wysiwyg-container .indicator {
.wysiwyg-container .selectionIndicator,
.wysiwyg-container .textboxIndicator {
display: inline-block;
display: none;
border: 1px solid black;
position: fixed;
z-index: 100;
pointer-events: none;
}
.wysiwyg-container .textboxIndicator {
border: 1px solid cyan;
}
.wysiwyg-container .textboxIndicator {
border: 1px solid magenta;
}
</style>
@ -70,6 +82,7 @@ import {
getLinesAsFragments,
cloneNodeTreeSelective,
cleanWysiwygMarkup,
splitDomAtElementBoundaries,
} from "../js/helpers/wysiwyg";
// Mapping of HTML tag names to IRC format control characters
@ -185,7 +198,7 @@ export default {
});
this.$refs.input.addEventListener("keyup", () => {
this.updateSelectionIndicator();
this.updateIndicators();
});
// Formatting
@ -208,10 +221,11 @@ export default {
return !this.$refs.input.textContent;
},
getHtmlContent() {
// TODO: format & setter
// TODO Move <br> removal to cleanup and do it dom based instead of string based
this.cleanInputDom();
let html = this.$refs.input.innerHTML;
// TODO Move <br> removal to cleanup and do it dom based instead of string based
if (html.endsWith("<br>")) {
// Remove the last trailing newline
html = html.substring(0, html.length - 4);
@ -276,8 +290,6 @@ export default {
const html = this.$refs.input.innerHTML;
const ircFormat = toIrcFormat(html);
document.getElementById("input").value = ircFormat;
},
onInput() {
this.onChange();
@ -287,7 +299,9 @@ export default {
this.setAutoHeight();
}
this.updateSelectionIndicator();
this.updateIndicators();
this.$emit("change");
},
// Actions
@ -341,7 +355,6 @@ export default {
});
},
pickColor() {
// TODO: This should be broken up into parts
const sel = window.getSelection();
// If there is no selection do nothing (sel.type is `Caret`)
@ -349,13 +362,14 @@ export default {
return;
}
// Get the cursor positon
// Get the position for the color selector based on the cursor positon of the selection
const range = sel.getRangeAt(0);
const rect = range.getBoundingClientRect();
const pos = {x: rect.left, y: rect.top - 5};
// Open the color picker above the current selection
this.$refs.colorpicker.open(pos, (colors) => {
// Focus the input after the color selector has completed
this.focus();
// If the color picker was exited or no colors were chosen do nothing
@ -379,6 +393,8 @@ export default {
const currentSelection = range.extractContents();
// Clone the selected tree, remove any spans but keep their content
// This ensures that the selection to be colored will not contain
// any nested coloring
const newTree = cloneNodeTreeSelective(
currentSelection,
(el) => el.nodeName === "SPAN"
@ -392,18 +408,9 @@ export default {
// Insert the color span into the container
range.insertNode(span);
// Split the dom at the boundaries of the selection to break any possible parent spans
// This is done by removing and re-inserting the nodes before and after the selection
// Extract and re-insert everything before the users selection
range.setStart(this.$refs.input, 0);
range.setEndBefore(span);
range.insertNode(range.extractContents());
// Extract and re-insert everything after the users selection
range.selectNodeContents(this.$refs.input);
range.setStartAfter(span);
range.insertNode(range.extractContents());
// Split the dom preserving styles at the start and end of the range
// This prevents nesting between the selection boundary (span)
splitDomAtElementBoundaries(this.$refs.input, span, range);
if (span.parentNode.nodeName === "SPAN") {
// If still nested in a color tag, replace the parent with the current color
@ -415,21 +422,67 @@ export default {
range.setEndBefore(span);
});
},
updateSelectionIndicator() {
updateIndicators() {
// TODO: this is only for debugging
const sel = window.getSelection();
const range = sel.getRangeAt(0);
const rect = range.getBoundingClientRect();
const selectionRect = range.getBoundingClientRect();
const indicator = this.$refs.indicator;
this.$refs.selectionIndicator.style.top = selectionRect.top + "px";
this.$refs.selectionIndicator.style.left = selectionRect.left + "px";
this.$refs.selectionIndicator.style.width = selectionRect.width + "px";
this.$refs.selectionIndicator.style.height = selectionRect.height + "px";
indicator.style.top = rect.top + "px";
indicator.style.left = rect.left + "px";
indicator.style.width = rect.width + "px";
indicator.style.height = rect.height + "px";
const inputRect = this.$refs.input.getBoundingClientRect();
this.$refs.textboxIndicator.style.top = inputRect.top + "px";
this.$refs.textboxIndicator.style.left = inputRect.left + "px";
this.$refs.textboxIndicator.style.width = inputRect.width + "px";
this.$refs.textboxIndicator.style.height = inputRect.height + "px";
// const topDist = Math.abs(selectionRect.top - inputRect.top)
// const bottomDist = Math.abs(selectionRect.bottom - inputRect.bottom)
// console.log("===========================")
// // console.log("selectionRect", selectionRect);
// // console.log("inputRect", inputRect);
// console.log("topDist", topDist)
// console.log("bottomDist", bottomDist)
return false;
},
// Emit up and down events when on first or last line
navigate(direction) {
// TODO: this is only for debugging
const sel = window.getSelection();
const range = sel.getRangeAt(0);
const selectionRect = range.getBoundingClientRect();
const inputRect = this.$refs.input.getBoundingClientRect();
const topDist = Math.abs(selectionRect.top - inputRect.top);
const bottomDist = Math.abs(selectionRect.bottom - inputRect.bottom);
const threshhold = 3; // Max diff
if (
direction === "up" &&
(this.isEmpty() ||
topDist <= threshhold ||
sel.anchorNode === this.$refs.input ||
sel.anchorNode === this.$refs.input.firstChild)
) {
this.$emit("navigate", {direction: "up"});
}
if (
direction === "down" &&
(this.isEmpty() ||
bottomDist <= threshhold ||
sel.anchorNode === this.$refs.input ||
sel.anchorNode === this.$refs.input.lastChild)
) {
this.$emit("navigate", {direction: "down"});
}
},
},
};
</script>

View file

@ -68,6 +68,21 @@ export function getLinesAsFragments(element, range) {
return fragments;
}
export function splitDomAtElementBoundaries(root, element, range) {
// Split the dom at the boundaries of the selection to break any possible parent spans
// This is done by removing and re-inserting the nodes before and after the selection
// Extract and re-insert everything before the users selection
range.setStart(root, 0);
range.setEndBefore(element);
range.insertNode(range.extractContents());
// Extract and re-insert everything after the users selection
range.selectNodeContents(root);
range.setStartAfter(element);
range.insertNode(range.extractContents());
}
// Recursively clone a node tree and omit elements that
// dont pass the test while keeping their children
export function cloneNodeTreeSelective(from, omitTest) {