livebook/assets/js/hooks/headline.js
Jonatan Kłosko 902c993098
Adjustments (#1087)
* Fix horizontal scrollbar on smaller screens

* Apply navigation shortcuts without an additional roundtrip

* Shorten the data element selector

* Fix URL in changelog

* Return reference from handle_intellisense
2022-04-04 12:19:11 +02:00

140 lines
3.8 KiB
JavaScript

import { getAttributeOrThrow } from "../lib/attribute";
import { globalPubSub } from "../lib/pub_sub";
import { smoothlyScrollToElement } from "../lib/utils";
/**
* A hook managing notebook/section headline.
*
* Similarly to cells the headline is focus/insert enabled.
*
* ## Configuration
*
* * `data-focusable-id` - an identifier for the focus/insert
* navigation
*
* * `data-on-value-change` - name of the event pushed when the user
* edits heading value
*
* * `data-metadata` - additional value to send with the change event
*/
const Headline = {
mounted() {
this.props = this.getProps();
this.isFocused = false;
this.insertMode = false;
this.initializeHeadingEl();
this.unsubscribeFromNavigationEvents = globalPubSub.subscribe(
"navigation",
(event) => {
this.handleNavigationEvent(event);
}
);
},
updated() {
this.props = this.getProps();
this.initializeHeadingEl();
},
destroyed() {
this.unsubscribeFromNavigationEvents();
},
getProps() {
return {
focusableId: getAttributeOrThrow(this.el, "data-focusable-id"),
onValueChange: getAttributeOrThrow(this.el, "data-on-value-change"),
metadata: getAttributeOrThrow(this.el, "data-metadata"),
};
},
initializeHeadingEl() {
const headingEl = this.el.querySelector(`[data-el-heading]`);
if (headingEl === this.headingEl) {
return;
}
this.headingEl = headingEl;
// Make sure only plain text is pasted
this.headingEl.addEventListener("paste", (event) => {
event.preventDefault();
const text = event.clipboardData.getData("text/plain").replace("\n", " ");
document.execCommand("insertText", false, text);
});
// Ignore enter
this.headingEl.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
}
});
this.headingEl.addEventListener("blur", (event) => {
// Wait for other handlers to complete and if still in insert
// mode force focus
setTimeout(() => {
if (this.isFocused && this.insertMode) {
this.headingEl.focus();
moveSelectionToEnd(this.headingEl);
}
}, 0);
});
},
handleNavigationEvent(event) {
if (event.type === "element_focused") {
this.handleElementFocused(event.focusableId, event.scroll);
} else if (event.type === "insert_mode_changed") {
this.handleInsertModeChanged(event.enabled);
}
},
handleElementFocused(cellId, scroll) {
if (this.props.focusableId === cellId) {
this.isFocused = true;
this.el.setAttribute("data-js-focused", "");
if (scroll) {
smoothlyScrollToElement(this.el);
}
} else if (this.isFocused) {
this.isFocused = false;
this.el.removeAttribute("data-js-focused");
}
},
handleInsertModeChanged(insertMode) {
if (this.isFocused && !this.insertMode && insertMode) {
this.insertMode = insertMode;
// While in insert mode, ignore the incoming changes
this.el.setAttribute("phx-update", "ignore");
this.headingEl.setAttribute("contenteditable", "");
this.headingEl.focus();
moveSelectionToEnd(this.headingEl);
} else if (this.insertMode && !insertMode) {
this.insertMode = insertMode;
this.headingEl.removeAttribute("contenteditable");
this.el.removeAttribute("phx-update");
this.pushEvent(this.props.onValueChange, {
value: this.headingEl.textContent.trim(),
metadata: this.props.metadata,
});
}
},
};
function moveSelectionToEnd(element) {
const range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
export default Headline;