Skip to content

Instantly share code, notes, and snippets.

@RoryDuncan
Last active June 13, 2019 01:32
Show Gist options
  • Save RoryDuncan/c59c7d9e364a2e92d6643f11f8d62bce to your computer and use it in GitHub Desktop.
Save RoryDuncan/c59c7d9e364a2e92d6643f11f8d62bce to your computer and use it in GitHub Desktop.
Simple Focus-based Editor
//
// A class that manages the event lifecycle, sanitization, and UX resolution when editing an input.
// See `Editor.edit()` for primary API usage.
// Example code at bottom.
//
export default class Editor {
constructor(maxlength = 60) {
this.maxlength = maxlength;
this.el = null;
// bindings
this.keydownHandler = this.keydownHandler.bind(this);
this.blurHandler = this.blurHandler.bind(this);
}
//
// properties
//
get isEditing() {
return this.el !== null;
}
get value() {
if (this.isEditing) return this.el.value.trim();
return null;
}
get isValid() {
const value = this.value;
if (value === null) return false;
if (value.length === 0 || value.length > this.maxlength) return false;
if (value === this.initialValue) return false;
return true;
}
//
// methods
//
sanitize(input) {
return input.replace(/</gi, "&lt;").replace(/>/gi, "&gt;");
}
/** Begins editing the parameter input element, adding event listeners and capturing the initial value.
Upon the promise-rejection, Editor will reset it's value to the original value when `.edit()` was first called.
* @param {HtmlInputElement} el The element that the user will edit text within.
* @returns {Promise} returns a promise that resolves with the new value or rejects with a string reason on why it failed.
* @notes Calling `edit` when an edit is already in place will reject the previous edit and begin a new promise-cycle.
*/
edit(el) {
if (this.isEditing) this.end();
if (el === null) return;
const that = this;
this.el = el;
el.setAttribute("maxlength", this.maxlength);
this.initialValue = this.el.value;
this.el.setAttribute("placeholder", this.initialValue);
this.el.value = "";
el.classList.add("--editing");
el.focus();
// if we want to highlight the selection
// document.execCommand('selectAll', false, null);
el.addEventListener("blur", this.blurHandler);
el.addEventListener("keydown", this.keydownHandler);
this.promise = new Promise((resolve, reject) => this.setExecutor(resolve, reject));
return this.promise;
}
setExecutor(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}
end() {
const { el, isValid, value, initialValue } = this;
el.contentEditable = false;
el.classList.remove("--editing");
el.blur();
el.removeEventListener("blur", this.blurHandler);
el.removeEventListener("keydown", this.keydownHandler);
if (isValid) {
this.resolve(this.sanitize(value));
}
else {
el.value = initialValue;
this.reject("Invalid text");
}
this.el = null;
}
keydownHandler(e){
switch (e.key.toLowerCase()) {
case "enter":
e.preventDefault();
this.end();
return;
case "escape":
this.el.value = this.initialValue;
this.end();
return;
case "backspace":
case "delete":
case "arrowleft":
case "arrowright":
return;
}
}
blurHandler(e) {
this.end();
}
};
//
// Example usage
//
const editor = new Editor();
const someButton = document.getElementByTagName("button");
const someInput = document.getElementByTagName("input");
const store = new SomeDataStore(); // somewhere that we'd want to save the new value of an editor.
// when clicking the `someButton` button, begin editing our `someInput` input.
// different UI could represent this process, this is just for example.
someButton.addEventListener("click", e => editor.edit(someInput)
// upon the user pressing enter, the value is saved.
.then(newValue => store.save(newValue));
// upon the user bluring the input, pressing ESC, or entering invalid data, it will reject and we'll output why.
.catch(reason => console.log(`Exited editor: ${reason || "user aborted"}`)));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment