mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-29 19:20:46 +08:00
Refactor hook props parsing (#2369)
This commit is contained in:
parent
9aac5ce86d
commit
3bbf43c708
26 changed files with 280 additions and 365 deletions
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
getAttributeOrDefault,
|
||||
getAttributeOrThrow,
|
||||
parseInteger,
|
||||
} from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import { encodeAnnotatedBuffer, encodePcmAsWav } from "../lib/codec";
|
||||
|
||||
const dropClasses = ["bg-yellow-100", "border-yellow-300"];
|
||||
|
@ -10,19 +6,19 @@ const dropClasses = ["bg-yellow-100", "border-yellow-300"];
|
|||
/**
|
||||
* A hook for client-preprocessed audio input.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-id` - a unique id
|
||||
* * `id` - a unique id
|
||||
*
|
||||
* * `data-phx-target` - the component to send the `"change"` event to
|
||||
* * `phx-target` - the component to send the `"change"` event to
|
||||
*
|
||||
* * `data-format` - the desired audio format
|
||||
* * `format` - the desired audio format
|
||||
*
|
||||
* * `data-sampling-rate` - the audio sampling rate for
|
||||
* * `sampling-rate` - the audio sampling rate for
|
||||
*
|
||||
* * `data-endianness` - the server endianness, either `"little"` or `"big"`
|
||||
* * `endianness` - the server endianness, either `"little"` or `"big"`
|
||||
*
|
||||
* * `data-audio-url` - the URL to audio file to use for the current preview
|
||||
* * `audio-url` - the URL to audio file to use for the current preview
|
||||
*
|
||||
*/
|
||||
const AudioInput = {
|
||||
|
@ -103,18 +99,14 @@ const AudioInput = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
id: getAttributeOrThrow(this.el, "data-id"),
|
||||
phxTarget: getAttributeOrThrow(this.el, "data-phx-target", parseInteger),
|
||||
samplingRate: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-sampling-rate",
|
||||
parseInteger
|
||||
),
|
||||
endianness: getAttributeOrThrow(this.el, "data-endianness"),
|
||||
format: getAttributeOrThrow(this.el, "data-format"),
|
||||
audioUrl: getAttributeOrDefault(this.el, "data-audio-url", null),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"id",
|
||||
"phx-target",
|
||||
"sampling-rate",
|
||||
"endianness",
|
||||
"format",
|
||||
"audio-url",
|
||||
]);
|
||||
},
|
||||
|
||||
startRecording() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import Markdown from "../lib/markdown";
|
||||
import { globalPubSub } from "../lib/pub_sub";
|
||||
import { md5Base64, smoothlyScrollToElement } from "../lib/utils";
|
||||
|
@ -11,15 +11,23 @@ import { isEvaluable } from "../lib/notebook";
|
|||
* Manages the collaborative editor, takes care of markdown rendering
|
||||
* and focusing the editor when applicable.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-cell-id` - id of the cell being edited
|
||||
* * `cell-id` - id of the cell being edited
|
||||
*
|
||||
* * `data-type` - type of the cell
|
||||
* * `type` - type of the cell
|
||||
*
|
||||
* * `data-session-path` - root path to the current session
|
||||
* * `session-path` - root path to the current session
|
||||
*
|
||||
* * `evaluation-digest` - digest of the last evaluated cell source
|
||||
*
|
||||
* * `smart-cell-js-view-ref` - ref for the JS View, applicable
|
||||
* only to Smart cells
|
||||
*
|
||||
* * `allowed-uri-schemes` - a list of additional URI schemes that
|
||||
* should be kept during sanitization. Applicable only to Markdown
|
||||
* cells
|
||||
*
|
||||
* * `data-evaluation-digest` - digest of the last evaluated cell source
|
||||
*/
|
||||
const Cell = {
|
||||
mounted() {
|
||||
|
@ -124,25 +132,14 @@ const Cell = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
cellId: getAttributeOrThrow(this.el, "data-cell-id"),
|
||||
type: getAttributeOrThrow(this.el, "data-type"),
|
||||
sessionPath: getAttributeOrThrow(this.el, "data-session-path"),
|
||||
evaluationDigest: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-evaluation-digest",
|
||||
null
|
||||
),
|
||||
smartCellJSViewRef: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-smart-cell-js-view-ref",
|
||||
null
|
||||
),
|
||||
allowedUriSchemes: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-allowed-uri-schemes"
|
||||
),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"cell-id",
|
||||
"type",
|
||||
"session-path",
|
||||
"evaluation-digest",
|
||||
"smart-cell-js-view-ref",
|
||||
"allowed-uri-schemes",
|
||||
]);
|
||||
},
|
||||
|
||||
handleNavigationEvent(event) {
|
||||
|
@ -377,9 +374,9 @@ const Cell = {
|
|||
},
|
||||
|
||||
handleDispatchQueueEvaluation(dispatch) {
|
||||
if (this.props.type === "smart" && this.props.smartCellJSViewRef) {
|
||||
if (this.props.type === "smart" && this.props.smartCellJsViewRef) {
|
||||
// Ensure the smart cell UI is reflected on the server, before the evaluation
|
||||
globalPubSub.broadcast(`js_views:${this.props.smartCellJSViewRef}`, {
|
||||
globalPubSub.broadcast(`js_views:${this.props.smartCellJsViewRef}`, {
|
||||
type: "sync",
|
||||
callback: dispatch,
|
||||
});
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import LiveEditor from "./cell_editor/live_editor";
|
||||
import {
|
||||
getAttributeOrDefault,
|
||||
getAttributeOrThrow,
|
||||
parseBoolean,
|
||||
} from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
|
||||
const CellEditor = {
|
||||
mounted() {
|
||||
|
@ -71,17 +67,13 @@ const CellEditor = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
cellId: getAttributeOrThrow(this.el, "data-cell-id"),
|
||||
tag: getAttributeOrThrow(this.el, "data-tag"),
|
||||
language: getAttributeOrDefault(this.el, "data-language", null),
|
||||
intellisense: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-intellisense",
|
||||
parseBoolean
|
||||
),
|
||||
readOnly: getAttributeOrThrow(this.el, "data-read-only", parseBoolean),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"cell-id",
|
||||
"tag",
|
||||
"language",
|
||||
"intellisense",
|
||||
"read-only",
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import { globalPubSub } from "../lib/pub_sub";
|
||||
import { smoothlyScrollToElement } from "../lib/utils";
|
||||
|
||||
|
@ -7,15 +7,15 @@ import { smoothlyScrollToElement } from "../lib/utils";
|
|||
*
|
||||
* Similarly to cells the headline is focus/insert enabled.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-focusable-id` - an identifier for the focus/insert
|
||||
* navigation
|
||||
* * `id` - an identifier for the focus/insert navigation
|
||||
*
|
||||
* * `data-on-value-change` - name of the event pushed when the user
|
||||
* * `on-value-change` - name of the event pushed when the user
|
||||
* edits heading value
|
||||
*
|
||||
* * `data-metadata` - additional value to send with the change event
|
||||
* * `metadata` - additional value to send with the change event
|
||||
*
|
||||
*/
|
||||
const Headline = {
|
||||
mounted() {
|
||||
|
@ -44,11 +44,7 @@ const Headline = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
focusableId: getAttributeOrThrow(this.el, "data-focusable-id"),
|
||||
onValueChange: getAttributeOrThrow(this.el, "data-on-value-change"),
|
||||
metadata: getAttributeOrThrow(this.el, "data-metadata"),
|
||||
};
|
||||
return parseHookProps(this.el, ["id", "on-value-change", "metadata"]);
|
||||
},
|
||||
|
||||
initializeHeadingEl() {
|
||||
|
@ -94,8 +90,8 @@ const Headline = {
|
|||
}
|
||||
},
|
||||
|
||||
handleElementFocused(cellId, scroll) {
|
||||
if (this.props.focusableId === cellId) {
|
||||
handleElementFocused(focusableId, scroll) {
|
||||
if (this.props.id === focusableId) {
|
||||
this.isFocused = true;
|
||||
this.el.setAttribute("data-js-focused", "");
|
||||
if (scroll) {
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import { highlight } from "./cell_editor/live_editor/monaco";
|
||||
import { findChildOrThrow } from "../lib/utils";
|
||||
|
||||
/**
|
||||
* A hook used to highlight source code in the root element.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-language` - language of the source code
|
||||
* * `language` - language of the source code
|
||||
*
|
||||
* The element should have two children:
|
||||
* ## Children
|
||||
*
|
||||
* * `[data-source]` - an element containing the source code to be
|
||||
* highlighted
|
||||
*
|
||||
* * `[data-target]` - the element to render highlighted code into
|
||||
*
|
||||
*/
|
||||
const Highlight = {
|
||||
mounted() {
|
||||
|
@ -32,9 +33,7 @@ const Highlight = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
language: getAttributeOrThrow(this.el, "data-language"),
|
||||
};
|
||||
return parseHookProps(this.el, ["language"]);
|
||||
},
|
||||
|
||||
updateDOM() {
|
||||
|
|
|
@ -1,34 +1,29 @@
|
|||
import {
|
||||
getAttributeOrDefault,
|
||||
getAttributeOrThrow,
|
||||
parseInteger,
|
||||
} from "../lib/attribute";
|
||||
import { encodeAnnotatedBuffer } from "../lib/codec";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
|
||||
const dropClasses = ["bg-yellow-100", "border-yellow-300"];
|
||||
|
||||
/**
|
||||
* A hook for client-preprocessed image input.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-id` - a unique id
|
||||
* * `id` - a unique id
|
||||
*
|
||||
* * `data-phx-target` - the component to send the `"change"` event to
|
||||
* * `phx-target` - the component to send the `"change"` event to
|
||||
*
|
||||
* * `data-height` - the image bounding height
|
||||
* * `height` - the image bounding height
|
||||
*
|
||||
* * `data-width` - the image bounding width
|
||||
* * `width` - the image bounding width
|
||||
*
|
||||
* * `data-format` - the desired image format
|
||||
* * `format` - the desired image format
|
||||
*
|
||||
* * `data-fit` - the fit strategy
|
||||
* * `fit` - the fit strategy
|
||||
*
|
||||
* * `data-image-url` - the URL to the image binary value
|
||||
* * `image-url` - the URL to the image binary value
|
||||
*
|
||||
* * `data-value-height` - the height of the current image value
|
||||
* * `value-height` - the height of the current image value
|
||||
*
|
||||
* * `data-value-width` - the width fo the current image value
|
||||
* * `value-width` - the width fo the current image value
|
||||
*
|
||||
*/
|
||||
const ImageInput = {
|
||||
|
@ -139,27 +134,17 @@ const ImageInput = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
id: getAttributeOrThrow(this.el, "data-id"),
|
||||
phxTarget: getAttributeOrThrow(this.el, "data-phx-target", parseInteger),
|
||||
height: getAttributeOrDefault(this.el, "data-height", null, parseInteger),
|
||||
width: getAttributeOrDefault(this.el, "data-width", null, parseInteger),
|
||||
format: getAttributeOrThrow(this.el, "data-format"),
|
||||
fit: getAttributeOrThrow(this.el, "data-fit"),
|
||||
imageUrl: getAttributeOrDefault(this.el, "data-image-url", null),
|
||||
valueHeight: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-value-height",
|
||||
null,
|
||||
parseInteger
|
||||
),
|
||||
valueWidth: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-value-width",
|
||||
null,
|
||||
parseInteger
|
||||
),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"id",
|
||||
"phx-target",
|
||||
"height",
|
||||
"width",
|
||||
"format",
|
||||
"fit",
|
||||
"image-url",
|
||||
"value-height",
|
||||
"value-width",
|
||||
]);
|
||||
},
|
||||
|
||||
updateImagePreview() {
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
getAttributeOrDefault,
|
||||
getAttributeOrThrow,
|
||||
parseInteger,
|
||||
} from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import {
|
||||
isElementHidden,
|
||||
isElementVisibleInViewport,
|
||||
|
@ -36,32 +32,31 @@ import { initializeIframeSource } from "./js_view/iframe";
|
|||
* Then, a number of `event:<ref>` with `{ event, payload }` payload
|
||||
* can be sent. The `event` is forwarded to the initialized component.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-ref` - a unique identifier used as messages scope
|
||||
* * `ref` - a unique identifier used as messages scope
|
||||
*
|
||||
* * `data-assets-base-path` - the base path to fetch assets from
|
||||
* * `assets-base-path` - the base path to fetch assets from
|
||||
* within the iframe (and resolve all relative paths against)
|
||||
*
|
||||
* * `data-assets-cdn-url` - a URL to CDN location to fetch assets
|
||||
* * `assets-cdn-url` - a URL to CDN location to fetch assets
|
||||
* from. Only used if specified and the entrypoint script can be
|
||||
* successfully accessed, also only when Livebook runs on https
|
||||
*
|
||||
* * `data-js-path` - a relative path for the initial view-specific
|
||||
* * `js-path` - a relative path for the initial view-specific
|
||||
* JS module
|
||||
*
|
||||
* * `data-session-token` - a session-specific token passed when
|
||||
* * `session-token` - a session-specific token passed when
|
||||
* joining the JS view channel
|
||||
*
|
||||
* * `data-connect-token` - a JS view specific token passed in the
|
||||
* * `connect-token` - a JS view specific token passed in the
|
||||
* "connect" message to the channel
|
||||
*
|
||||
* * `data-iframe-local-port` - the local port where the iframe is
|
||||
* served
|
||||
* * `iframe-port` - the local port where the iframe is served
|
||||
*
|
||||
* * `data-iframe-url` - an optional location to load the iframe from
|
||||
* * `iframe-url` - an optional location to load the iframe from
|
||||
*
|
||||
* * `data-timeout-message` - the message to show when the initial
|
||||
* * `timeout-message` - the message to show when the initial
|
||||
* data does not load
|
||||
*
|
||||
*/
|
||||
|
@ -180,21 +175,17 @@ const JSView = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
ref: getAttributeOrThrow(this.el, "data-ref"),
|
||||
assetsBasePath: getAttributeOrThrow(this.el, "data-assets-base-path"),
|
||||
assetsCdnUrl: getAttributeOrDefault(this.el, "data-assets-cdn-url", null),
|
||||
jsPath: getAttributeOrThrow(this.el, "data-js-path"),
|
||||
sessionToken: getAttributeOrThrow(this.el, "data-session-token"),
|
||||
connectToken: getAttributeOrThrow(this.el, "data-connect-token"),
|
||||
iframePort: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-iframe-local-port",
|
||||
parseInteger
|
||||
),
|
||||
iframeUrl: getAttributeOrDefault(this.el, "data-iframe-url", null),
|
||||
timeoutMessage: getAttributeOrThrow(this.el, "data-timeout-message"),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"ref",
|
||||
"assets-base-path",
|
||||
"assets-cdn-url",
|
||||
"js-path",
|
||||
"session-token",
|
||||
"connect-token",
|
||||
"iframe-port",
|
||||
"iframe-url",
|
||||
"timeout-message",
|
||||
]);
|
||||
},
|
||||
|
||||
createIframe() {
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import { getAttributeOrThrow, parseBoolean } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import { cancelEvent, isEditableElement, isMacOS } from "../lib/utils";
|
||||
|
||||
/**
|
||||
* A hook for ControlComponent to handle user keyboard interactions.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-cell-id` - id of the cell in which the control is rendered
|
||||
* * `cell-id` - id of the cell in which the control is rendered
|
||||
*
|
||||
* * `data-default-handlers` - whether keyboard events should be
|
||||
* * `default-handlers` - whether keyboard events should be
|
||||
* intercepted and canceled, disabling session shortcuts. Must be
|
||||
* one of "off", "on", or "disable_only"
|
||||
*
|
||||
* * `data-keydown-enabled` - whether keydown events should be listened to
|
||||
* * `keydown-enabled` - whether keydown events should be listened to
|
||||
*
|
||||
* * `data-keyup-enabled` - whether keyup events should be listened to
|
||||
* * `keyup-enabled` - whether keyup events should be listened to
|
||||
*
|
||||
* * `target` - the target to send live events to
|
||||
*
|
||||
* * `data-target` - the target to send live events to
|
||||
*/
|
||||
const KeyboardControl = {
|
||||
mounted() {
|
||||
|
@ -47,21 +48,13 @@ const KeyboardControl = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
cellId: getAttributeOrThrow(this.el, "data-cell-id"),
|
||||
defaultHandlers: getAttributeOrThrow(this.el, "data-default-handlers"),
|
||||
isKeydownEnabled: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-keydown-enabled",
|
||||
parseBoolean
|
||||
),
|
||||
isKeyupEnabled: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-keyup-enabled",
|
||||
parseBoolean
|
||||
),
|
||||
target: getAttributeOrThrow(this.el, "data-target"),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"cell-id",
|
||||
"default-handlers",
|
||||
"keydown-enabled",
|
||||
"keyup-enabled",
|
||||
"target",
|
||||
]);
|
||||
},
|
||||
|
||||
handleDocumentKeyDown(event) {
|
||||
|
@ -83,7 +76,7 @@ const KeyboardControl = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.props.isKeydownEnabled) {
|
||||
if (this.props.keydownEnabled) {
|
||||
const { key } = event;
|
||||
this.pushEventTo(this.props.target, "keydown", { key });
|
||||
}
|
||||
|
@ -96,7 +89,7 @@ const KeyboardControl = {
|
|||
cancelEvent(event);
|
||||
}
|
||||
|
||||
if (this.props.isKeyupEnabled) {
|
||||
if (this.props.keyupEnabled) {
|
||||
const { key } = event;
|
||||
this.pushEventTo(this.props.target, "keyup", { key });
|
||||
}
|
||||
|
@ -104,7 +97,7 @@ const KeyboardControl = {
|
|||
},
|
||||
|
||||
handleDocumentFocus(event) {
|
||||
if (this.props.isKeydownEnabled && isEditableElement(event.target)) {
|
||||
if (this.props.keydownEnabled && isEditableElement(event.target)) {
|
||||
this.disableKeyboard();
|
||||
}
|
||||
},
|
||||
|
@ -122,7 +115,7 @@ const KeyboardControl = {
|
|||
},
|
||||
|
||||
keyboardEnabled() {
|
||||
return this.props.isKeydownEnabled || this.props.isKeyupEnabled;
|
||||
return this.props.keydownEnabled || this.props.keyupEnabled;
|
||||
},
|
||||
|
||||
isKeyboardToggle(event) {
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import Markdown from "../lib/markdown";
|
||||
import { findChildOrThrow } from "../lib/utils";
|
||||
|
||||
/**
|
||||
* A hook used to render Markdown content on the client.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-base-path` - the path to resolve relative URLs against
|
||||
* * `base-path` - the path to resolve relative URLs against
|
||||
*
|
||||
* * `data-allowed-uri-schemes` - a comma separated list of additional
|
||||
* URI schemes that should be kept during sanitization
|
||||
* * `allowed-uri-schemes` - a list of additional URI schemes
|
||||
* that should be kept during sanitization
|
||||
*
|
||||
* The element should have two children:
|
||||
* ## Children
|
||||
*
|
||||
* * `[data-template]` - a hidden container containing the markdown
|
||||
* content. The DOM structure is ignored, only text content matters
|
||||
|
@ -29,7 +29,7 @@ const MarkdownRenderer = {
|
|||
|
||||
this.markdown = new Markdown(this.contentEl, this.templateEl.textContent, {
|
||||
baseUrl: this.props.basePath,
|
||||
allowedUriSchemes: this.props.allowedUriSchemes.split(","),
|
||||
allowedUriSchemes: this.props.allowedUriSchemes,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -40,13 +40,7 @@ const MarkdownRenderer = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
basePath: getAttributeOrThrow(this.el, "data-base-path"),
|
||||
allowedUriSchemes: getAttributeOrThrow(
|
||||
this.el,
|
||||
"data-allowed-uri-schemes"
|
||||
),
|
||||
};
|
||||
return parseHookProps(this.el, ["base-path", "allowed-uri-schemes"]);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
isElementInViewport,
|
||||
isElementHidden,
|
||||
} from "../lib/utils";
|
||||
import { getAttributeOrDefault } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import KeyBuffer from "../lib/key_buffer";
|
||||
import { globalPubSub } from "../lib/pub_sub";
|
||||
import monaco from "./cell_editor/live_editor/monaco";
|
||||
|
@ -26,11 +26,13 @@ import { settingsStore } from "../lib/settings";
|
|||
* communicate between this global hook and cells and for that we
|
||||
* use a simple local pubsub that the hooks subscribe to.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-autofocus-cell-id` - id of the cell that gets initial
|
||||
* * `autofocus-cell-id` - id of the cell that gets initial
|
||||
* focus once the notebook is loaded
|
||||
*
|
||||
* * `global-status` - global evaluation status
|
||||
*
|
||||
* ## Shortcuts
|
||||
*
|
||||
* This hook registers session shortcut handlers,
|
||||
|
@ -270,14 +272,7 @@ const Session = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
autofocusCellId: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-autofocus-cell-id",
|
||||
null
|
||||
),
|
||||
globalStatus: getAttributeOrDefault(this.el, "data-global-status", null),
|
||||
};
|
||||
return parseHookProps(this.el, ["autofocus-cell-id", "global-status"]);
|
||||
},
|
||||
|
||||
faviconForEvaluationStatus(evaluationStatus) {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
|
||||
const UPDATE_INTERVAL_MS = 100;
|
||||
|
||||
/**
|
||||
* A hook used to display a counting timer.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `start` - the timestamp to count from
|
||||
*
|
||||
* * `data-start` - the timestamp to count from
|
||||
*/
|
||||
const Timer = {
|
||||
mounted() {
|
||||
|
@ -26,9 +27,7 @@ const Timer = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
start: getAttributeOrThrow(this.el, "data-start"),
|
||||
};
|
||||
return parseHookProps(this.el, ["start"]);
|
||||
},
|
||||
|
||||
updateDOM() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
|
||||
/**
|
||||
* A hook for client-preprocessed datetime input.
|
||||
|
@ -20,12 +20,12 @@ const UtcDateTimeInput = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
utcValue: getAttributeOrDefault(this.el, "data-utc-value", null),
|
||||
utcMin: getAttributeOrDefault(this.el, "data-utc-min", null),
|
||||
utcMax: getAttributeOrDefault(this.el, "data-utc-max", null),
|
||||
phxTarget: getAttributeOrThrow(this.el, "data-phx-target"),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"utc-value",
|
||||
"utc-min",
|
||||
"utc-max",
|
||||
"phx-target",
|
||||
]);
|
||||
},
|
||||
|
||||
updateAttrs() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
|
||||
/**
|
||||
* A hook for client-preprocessed time input.
|
||||
|
@ -20,12 +20,12 @@ const UtcTimeInput = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
utcValue: getAttributeOrDefault(this.el, "data-utc-value", null),
|
||||
utcMin: getAttributeOrDefault(this.el, "data-utc-min", null),
|
||||
utcMax: getAttributeOrDefault(this.el, "data-utc-max", null),
|
||||
phxTarget: getAttributeOrThrow(this.el, "data-phx-target"),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"utc-value",
|
||||
"utc-min",
|
||||
"utc-max",
|
||||
"phx-target",
|
||||
]);
|
||||
},
|
||||
|
||||
updateAttrs() {
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import HyperList from "hyperlist";
|
||||
import {
|
||||
getAttributeOrDefault,
|
||||
getAttributeOrThrow,
|
||||
parseBoolean,
|
||||
parseInteger,
|
||||
} from "../lib/attribute";
|
||||
import { parseHookProps } from "../lib/attribute";
|
||||
import {
|
||||
findChildOrThrow,
|
||||
getLineHeight,
|
||||
|
@ -16,21 +11,21 @@ import {
|
|||
* A hook used to render text lines as a virtual list, so that only
|
||||
* the visible lines are actually in the DOM.
|
||||
*
|
||||
* ## Configuration
|
||||
* ## Props
|
||||
*
|
||||
* * `data-max-height` - the maximum height of the element, exceeding
|
||||
* * `max-height` - the maximum height of the element, exceeding
|
||||
* this height enables scrolling
|
||||
*
|
||||
* * `data-follow` - whether to automatically scroll to the bottom as
|
||||
* new lines appear. Defaults to false
|
||||
* * `follow` - whether to automatically scroll to the bottom as
|
||||
* new lines appear
|
||||
*
|
||||
* * `data-max-lines` - the maximum number of lines to keep in the DOM.
|
||||
* * `max-lines` - the maximum number of lines to keep in the DOM.
|
||||
* By default all lines are kept
|
||||
*
|
||||
* * `data-ignore-trailing-empty-line` - whether to ignore the last
|
||||
* line if it is empty. Defaults to false
|
||||
* * `ignore-trailing-empty-line` - whether to ignore the last
|
||||
* line if it is empty
|
||||
*
|
||||
* The element should have two children:
|
||||
* ## Children
|
||||
*
|
||||
* * `[data-template]` - a hidden container containing all the line
|
||||
* elements, each with a data-line attribute
|
||||
|
@ -73,27 +68,12 @@ const VirtualizedLines = {
|
|||
},
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
maxHeight: getAttributeOrThrow(this.el, "data-max-height", parseInteger),
|
||||
follow: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-follow",
|
||||
false,
|
||||
parseBoolean
|
||||
),
|
||||
maxLines: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-max-lines",
|
||||
null,
|
||||
parseInteger
|
||||
),
|
||||
ignoreTrailingEmptyLine: getAttributeOrDefault(
|
||||
this.el,
|
||||
"data-ignore-trailing-empty-line",
|
||||
false,
|
||||
parseBoolean
|
||||
),
|
||||
};
|
||||
return parseHookProps(this.el, [
|
||||
"max-height",
|
||||
"follow",
|
||||
"max-lines",
|
||||
"ignore-trailing-empty-line",
|
||||
]);
|
||||
},
|
||||
|
||||
hyperListConfig() {
|
||||
|
|
|
@ -1,49 +1,27 @@
|
|||
export function getAttributeOrThrow(element, attr, transform = null) {
|
||||
if (!element.hasAttribute(attr)) {
|
||||
throw new Error(
|
||||
`Missing attribute '${attr}' on element <${element.tagName}:${element.id}>`
|
||||
);
|
||||
}
|
||||
export function parseHookProps(element, names) {
|
||||
const props = {};
|
||||
|
||||
const value = element.getAttribute(attr);
|
||||
for (const name of names) {
|
||||
const attr = `data-p-${name}`;
|
||||
|
||||
return transform ? transform(value) : value;
|
||||
}
|
||||
if (!element.hasAttribute(attr)) {
|
||||
throw new Error(
|
||||
`Missing attribute "${attr}" on element <${element.tagName}:${element.id}>`
|
||||
);
|
||||
}
|
||||
|
||||
export function getAttributeOrDefault(
|
||||
element,
|
||||
attr,
|
||||
defaultValue,
|
||||
transform = null
|
||||
) {
|
||||
if (element.hasAttribute(attr)) {
|
||||
const value = element.getAttribute(attr);
|
||||
return transform ? transform(value) : value;
|
||||
} else {
|
||||
return defaultValue;
|
||||
props[kebabToCamelCase(name)] = JSON.parse(value);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
export function parseBoolean(value) {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
function kebabToCamelCase(name) {
|
||||
const [part, ...parts] = name.split("-");
|
||||
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid boolean attribute ${value}, should be either "true" or "false"`
|
||||
);
|
||||
}
|
||||
|
||||
export function parseInteger(value) {
|
||||
const number = parseInt(value, 10);
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
throw new Error(`Invalid integer value ${value}`);
|
||||
}
|
||||
|
||||
return number;
|
||||
return [
|
||||
part,
|
||||
...parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)),
|
||||
].join("");
|
||||
}
|
||||
|
|
|
@ -476,7 +476,7 @@ defmodule LivebookWeb.CoreComponents do
|
|||
class={if(@wrap, do: "break-all whitespace-pre-wrap", else: "tiny-scrollbar")}
|
||||
id={"#{@source_id}-highlight"}
|
||||
phx-hook="Highlight"
|
||||
data-language={@language}
|
||||
data-p-language={hook_prop(@language)}
|
||||
><div id={@source_id} data-source><%= @source %></div><div data-target></div></code></pre>
|
||||
</div>
|
||||
"""
|
||||
|
@ -719,4 +719,23 @@ defmodule LivebookWeb.CoreComponents do
|
|||
|
||||
Phoenix.LiveView.push_event(socket, "lb:exec_js", %{js: Jason.encode!(js.ops), to: opts[:to]})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes value for hook prop attribute.
|
||||
|
||||
## Examples
|
||||
|
||||
<div id="hook" phx-hook={MyHook} data-p-value={hook_prop(@value)}>
|
||||
</div>
|
||||
|
||||
"""
|
||||
def hook_prop(value)
|
||||
|
||||
def hook_prop(%Phoenix.LiveComponent.CID{} = value) do
|
||||
hook_prop(to_string(value))
|
||||
end
|
||||
|
||||
def hook_prop(value) do
|
||||
Jason.encode!(value)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,15 +16,17 @@ defmodule LivebookWeb.JSViewComponent do
|
|||
id={"js-output-#{@id}-#{@js_view.ref}"}
|
||||
phx-hook="JSView"
|
||||
phx-update="ignore"
|
||||
data-ref={@js_view.ref}
|
||||
data-assets-base-path={~p"/public/sessions/#{@session_id}/assets/#{@js_view.assets.hash}/"}
|
||||
data-assets-cdn-url={cdn_url(@js_view.assets[:cdn_url])}
|
||||
data-js-path={@js_view.assets.js_path}
|
||||
data-session-token={session_token(@session_id, @client_id)}
|
||||
data-connect-token={connect_token(@js_view.pid)}
|
||||
data-iframe-local-port={LivebookWeb.IframeEndpoint.port()}
|
||||
data-iframe-url={Livebook.Config.iframe_url()}
|
||||
data-timeout-message={@timeout_message}
|
||||
data-p-ref={hook_prop(@js_view.ref)}
|
||||
data-p-assets-base-path={
|
||||
hook_prop(~p"/public/sessions/#{@session_id}/assets/#{@js_view.assets.hash}/")
|
||||
}
|
||||
data-p-assets-cdn-url={hook_prop(cdn_url(@js_view.assets[:cdn_url]))}
|
||||
data-p-js-path={hook_prop(@js_view.assets.js_path)}
|
||||
data-p-session-token={hook_prop(session_token(@session_id, @client_id))}
|
||||
data-p-connect-token={hook_prop(connect_token(@js_view.pid))}
|
||||
data-p-iframe-port={hook_prop(LivebookWeb.IframeEndpoint.port())}
|
||||
data-p-iframe-url={hook_prop(Livebook.Config.iframe_url())}
|
||||
data-p-timeout-message={hook_prop(@timeout_message)}
|
||||
>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
@ -59,12 +59,12 @@ defmodule LivebookWeb.Output.AudioInputComponent do
|
|||
id={"#{@id}-root"}
|
||||
phx-hook="AudioInput"
|
||||
phx-update="ignore"
|
||||
data-id={@id}
|
||||
data-phx-target={@myself}
|
||||
data-format={@format}
|
||||
data-sampling-rate={@sampling_rate}
|
||||
data-endianness={@endianness}
|
||||
data-audio-url={@audio_url}
|
||||
data-p-id={hook_prop(@id)}
|
||||
data-p-phx-target={hook_prop(@myself)}
|
||||
data-p-format={hook_prop(@format)}
|
||||
data-p-sampling-rate={hook_prop(@sampling_rate)}
|
||||
data-p-endianness={hook_prop(@endianness)}
|
||||
data-p-audio-url={hook_prop(@audio_url)}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
|
|
|
@ -13,11 +13,11 @@ defmodule LivebookWeb.Output.ControlComponent do
|
|||
class="flex"
|
||||
id={"#{@id}-root"}
|
||||
phx-hook="KeyboardControl"
|
||||
data-cell-id={@cell_id}
|
||||
data-default-handlers={@control.attrs.default_handlers}
|
||||
data-keydown-enabled={to_string(@keyboard_enabled and :keydown in @control.attrs.events)}
|
||||
data-keyup-enabled={to_string(@keyboard_enabled and :keyup in @control.attrs.events)}
|
||||
data-target={@myself}
|
||||
data-p-cell-id={hook_prop(@cell_id)}
|
||||
data-p-default-handlers={hook_prop(@control.attrs.default_handlers)}
|
||||
data-p-keydown-enabled={hook_prop(@keyboard_enabled and :keydown in @control.attrs.events)}
|
||||
data-p-keyup-enabled={hook_prop(@keyboard_enabled and :keyup in @control.attrs.events)}
|
||||
data-p-target={hook_prop(@myself)}
|
||||
>
|
||||
<span class="tooltip right" data-tooltip="Toggle keyboard control">
|
||||
<button
|
||||
|
|
|
@ -55,15 +55,15 @@ defmodule LivebookWeb.Output.ImageInputComponent do
|
|||
class="inline-flex flex-col"
|
||||
phx-hook="ImageInput"
|
||||
phx-update="ignore"
|
||||
data-id={@id}
|
||||
data-phx-target={@myself}
|
||||
data-height={@height}
|
||||
data-width={@width}
|
||||
data-format={@format}
|
||||
data-fit={@fit}
|
||||
data-image-url={@image_url}
|
||||
data-value-height={@value[:height]}
|
||||
data-value-width={@value[:width]}
|
||||
data-p-id={hook_prop(@id)}
|
||||
data-p-phx-target={hook_prop(@myself)}
|
||||
data-p-height={hook_prop(@height)}
|
||||
data-p-width={hook_prop(@width)}
|
||||
data-p-format={hook_prop(@format)}
|
||||
data-p-fit={hook_prop(@fit)}
|
||||
data-p-image-url={hook_prop(@image_url)}
|
||||
data-p-value-height={hook_prop(@value[:height])}
|
||||
data-p-value-width={hook_prop(@value[:width])}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
|
|
|
@ -101,10 +101,10 @@ defmodule LivebookWeb.Output.InputComponent do
|
|||
step="60"
|
||||
autocomplete="off"
|
||||
phx-hook="UtcDateTimeInput"
|
||||
data-utc-value={@value && NaiveDateTime.to_iso8601(@value)}
|
||||
data-utc-min={@input.attrs.min && NaiveDateTime.to_iso8601(@input.attrs.min)}
|
||||
data-utc-max={@input.attrs.max && NaiveDateTime.to_iso8601(@input.attrs.max)}
|
||||
data-phx-target={@myself}
|
||||
data-p-utc-value={hook_prop(@value && NaiveDateTime.to_iso8601(@value))}
|
||||
data-p-utc-min={hook_prop(@input.attrs.min && NaiveDateTime.to_iso8601(@input.attrs.min))}
|
||||
data-p-utc-max={hook_prop(@input.attrs.max && NaiveDateTime.to_iso8601(@input.attrs.max))}
|
||||
data-p-phx-target={hook_prop(@myself)}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
|
@ -127,10 +127,10 @@ defmodule LivebookWeb.Output.InputComponent do
|
|||
step="60"
|
||||
autocomplete="off"
|
||||
phx-hook="UtcTimeInput"
|
||||
data-utc-value={@value && Time.to_iso8601(@value)}
|
||||
data-utc-min={@input.attrs.min && Time.to_iso8601(@input.attrs.min)}
|
||||
data-utc-max={@input.attrs.max && Time.to_iso8601(@input.attrs.max)}
|
||||
data-phx-target={@myself}
|
||||
data-p-utc-value={hook_prop(@value && Time.to_iso8601(@value))}
|
||||
data-p-utc-min={hook_prop(@input.attrs.min && Time.to_iso8601(@input.attrs.min))}
|
||||
data-p-utc-max={hook_prop(@input.attrs.max && Time.to_iso8601(@input.attrs.max))}
|
||||
data-p-phx-target={hook_prop(@myself)}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
@ -29,8 +29,8 @@ defmodule LivebookWeb.Output.MarkdownComponent do
|
|||
<div
|
||||
id={@id}
|
||||
phx-hook="MarkdownRenderer"
|
||||
data-base-path={~p"/sessions/#{@session_id}"}
|
||||
data-allowed-uri-schemes={Enum.join(@allowed_uri_schemes, ",")}
|
||||
data-p-base-path={hook_prop(~p"/sessions/#{@session_id}")}
|
||||
data-p-allowed-uri-schemes={hook_prop(@allowed_uri_schemes)}
|
||||
>
|
||||
<div
|
||||
data-template
|
||||
|
|
|
@ -53,10 +53,10 @@ defmodule LivebookWeb.Output.TerminalTextComponent do
|
|||
id={@id}
|
||||
class="relative"
|
||||
phx-hook="VirtualizedLines"
|
||||
data-max-height="300"
|
||||
data-follow="true"
|
||||
data-max-lines={Livebook.Notebook.max_terminal_lines()}
|
||||
data-ignore-trailing-empty-line="true"
|
||||
data-p-max-height={hook_prop(300)}
|
||||
data-p-follow={hook_prop(true)}
|
||||
data-p-max-lines={hook_prop(Livebook.Notebook.max_terminal_lines())}
|
||||
data-p-ignore-trailing-empty-line={hook_prop(true)}
|
||||
>
|
||||
<% # Note 1: We add a newline to each element, so that multiple lines can be copied properly as element.textContent %>
|
||||
<% # Note 2: We glue the tags together to avoid inserting unintended whitespace %>
|
||||
|
|
|
@ -133,8 +133,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
id={"session-#{@session.id}"}
|
||||
data-el-session
|
||||
phx-hook="Session"
|
||||
data-global-status={elem(@data_view.global_status, 0)}
|
||||
data-autofocus-cell-id={@autofocus_cell_id}
|
||||
data-p-global-status={hook_prop(elem(@data_view.global_status, 0))}
|
||||
data-p-autofocus-cell-id={hook_prop(@autofocus_cell_id)}
|
||||
>
|
||||
<nav
|
||||
class="w-16 flex flex-col items-center px-3 py-1 space-y-2 sm:space-y-3 sm:py-5 bg-gray-900"
|
||||
|
@ -287,8 +287,9 @@ defmodule LivebookWeb.SessionLive do
|
|||
data-focusable-id="notebook"
|
||||
id="notebook"
|
||||
phx-hook="Headline"
|
||||
data-on-value-change="set_notebook_name"
|
||||
data-metadata="notebook"
|
||||
data-p-id={hook_prop("notebook")}
|
||||
data-p-on-value-change={hook_prop("set_notebook_name")}
|
||||
data-p-metadata={hook_prop("notebook")}
|
||||
>
|
||||
<h1
|
||||
class="px-1 -ml-1.5 text-3xl font-semibold text-gray-800 border border-transparent rounded-lg whitespace-pre-wrap"
|
||||
|
|
|
@ -36,17 +36,18 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
class="flex flex-col relative scroll-mt-[50px] sm:scroll-mt-0"
|
||||
data-el-cell
|
||||
id={"cell-#{@cell_view.id}"}
|
||||
phx-hook="Cell"
|
||||
data-cell-id={@cell_view.id}
|
||||
data-focusable-id={@cell_view.id}
|
||||
data-type={@cell_view.type}
|
||||
data-session-path={~p"/sessions/#{@session_id}"}
|
||||
data-evaluation-digest={get_in(@cell_view, [:eval, :evaluation_digest])}
|
||||
data-focusable-id={@cell_view.id}
|
||||
data-js-empty={@cell_view.empty}
|
||||
data-eval-validity={get_in(@cell_view, [:eval, :validity])}
|
||||
data-eval-errored={get_in(@cell_view, [:eval, :errored])}
|
||||
data-js-empty={@cell_view.empty}
|
||||
data-smart-cell-js-view-ref={smart_cell_js_view_ref(@cell_view)}
|
||||
data-allowed-uri-schemes={Enum.join(@allowed_uri_schemes, ",")}
|
||||
phx-hook="Cell"
|
||||
data-p-cell-id={hook_prop(@cell_view.id)}
|
||||
data-p-type={hook_prop(@cell_view.type)}
|
||||
data-p-session-path={hook_prop(~p"/sessions/#{@session_id}")}
|
||||
data-p-evaluation-digest={hook_prop(get_in(@cell_view, [:eval, :evaluation_digest]))}
|
||||
data-p-smart-cell-js-view-ref={hook_prop(smart_cell_js_view_ref(@cell_view))}
|
||||
data-p-allowed-uri-schemes={hook_prop(@allowed_uri_schemes)}
|
||||
>
|
||||
<%= render_cell(assigns) %>
|
||||
</div>
|
||||
|
@ -621,11 +622,11 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
id={"cell-editor-#{@cell_id}-#{@tag}"}
|
||||
phx-update="ignore"
|
||||
phx-hook="CellEditor"
|
||||
data-cell-id={@cell_id}
|
||||
data-tag={@tag}
|
||||
data-language={@language}
|
||||
data-intellisense={to_string(@intellisense)}
|
||||
data-read-only={to_string(@read_only)}
|
||||
data-p-cell-id={hook_prop(@cell_id)}
|
||||
data-p-tag={hook_prop(@tag)}
|
||||
data-p-language={hook_prop(@language)}
|
||||
data-p-intellisense={hook_prop(@intellisense)}
|
||||
data-p-read-only={hook_prop(@read_only)}
|
||||
>
|
||||
<div class={["py-3 bg-editor", rounded_class(@rounded)]} data-el-editor-container>
|
||||
<div class="px-8" data-el-skeleton>
|
||||
|
@ -687,7 +688,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
id={"#{@id}-cell-timer"}
|
||||
phx-hook="Timer"
|
||||
phx-update="ignore"
|
||||
data-start={DateTime.to_iso8601(@cell_view.eval.evaluation_start)}
|
||||
data-p-start={hook_prop(DateTime.to_iso8601(@cell_view.eval.evaluation_start))}
|
||||
>
|
||||
</span>
|
||||
</.cell_status_indicator>
|
||||
|
|
|
@ -12,8 +12,9 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
id={@section_view.id}
|
||||
data-focusable-id={@section_view.id}
|
||||
phx-hook="Headline"
|
||||
data-on-value-change="set_section_name"
|
||||
data-metadata={@section_view.id}
|
||||
data-p-id={hook_prop(@section_view.id)}
|
||||
data-p-on-value-change={hook_prop("set_section_name")}
|
||||
data-p-metadata={hook_prop(@section_view.id)}
|
||||
>
|
||||
<div class="absolute left-0 top-0 bottom-0 transform -translate-x-full w-10 flex justify-end items-center pr-2">
|
||||
<button
|
||||
|
|
Loading…
Reference in a new issue