Refactor hook props parsing (#2369)

This commit is contained in:
Jonatan Kłosko 2023-11-23 16:18:06 +01:00 committed by GitHub
parent 9aac5ce86d
commit 3bbf43c708
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 280 additions and 365 deletions

View file

@ -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() {

View file

@ -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,
});

View file

@ -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",
]);
},
};

View file

@ -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) {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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) {

View file

@ -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"]);
},
};

View file

@ -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) {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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("");
}

View file

@ -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

View file

@ -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>
"""

View file

@ -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"

View 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

View file

@ -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"

View 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>
"""

View file

@ -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

View file

@ -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 %>

View file

@ -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"

View file

@ -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>

View file

@ -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