diff --git a/assets/js/hooks/audio_input.js b/assets/js/hooks/audio_input.js index 6a3991cbd..9ac64ab82 100644 --- a/assets/js/hooks/audio_input.js +++ b/assets/js/hooks/audio_input.js @@ -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() { diff --git a/assets/js/hooks/cell.js b/assets/js/hooks/cell.js index fd8eb2945..2d51626e2 100644 --- a/assets/js/hooks/cell.js +++ b/assets/js/hooks/cell.js @@ -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, }); diff --git a/assets/js/hooks/cell_editor.js b/assets/js/hooks/cell_editor.js index d24b7a7d7..65e8cac36 100644 --- a/assets/js/hooks/cell_editor.js +++ b/assets/js/hooks/cell_editor.js @@ -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", + ]); }, }; diff --git a/assets/js/hooks/headline.js b/assets/js/hooks/headline.js index cc0acae24..fd35b7997 100644 --- a/assets/js/hooks/headline.js +++ b/assets/js/hooks/headline.js @@ -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) { diff --git a/assets/js/hooks/highlight.js b/assets/js/hooks/highlight.js index 96f5c5014..7743df20f 100644 --- a/assets/js/hooks/highlight.js +++ b/assets/js/hooks/highlight.js @@ -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() { diff --git a/assets/js/hooks/image_input.js b/assets/js/hooks/image_input.js index ce42d31c8..6576f7134 100644 --- a/assets/js/hooks/image_input.js +++ b/assets/js/hooks/image_input.js @@ -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() { diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index a891ba3c5..0c227b66d 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -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:` 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() { diff --git a/assets/js/hooks/keyboard_control.js b/assets/js/hooks/keyboard_control.js index c5f38551e..41151b1ea 100644 --- a/assets/js/hooks/keyboard_control.js +++ b/assets/js/hooks/keyboard_control.js @@ -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) { diff --git a/assets/js/hooks/markdown_renderer.js b/assets/js/hooks/markdown_renderer.js index bfc008bf1..32d083200 100644 --- a/assets/js/hooks/markdown_renderer.js +++ b/assets/js/hooks/markdown_renderer.js @@ -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"]); }, }; diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 34b41b991..2ad1c1abf 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -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) { diff --git a/assets/js/hooks/timer.js b/assets/js/hooks/timer.js index 7eb573fa9..e5ff563bb 100644 --- a/assets/js/hooks/timer.js +++ b/assets/js/hooks/timer.js @@ -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() { diff --git a/assets/js/hooks/utc_datetime_input.js b/assets/js/hooks/utc_datetime_input.js index 188b75b3e..52b4d977a 100644 --- a/assets/js/hooks/utc_datetime_input.js +++ b/assets/js/hooks/utc_datetime_input.js @@ -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() { diff --git a/assets/js/hooks/utc_time_input.js b/assets/js/hooks/utc_time_input.js index d2caf0749..f2adac98c 100644 --- a/assets/js/hooks/utc_time_input.js +++ b/assets/js/hooks/utc_time_input.js @@ -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() { diff --git a/assets/js/hooks/virtualized_lines.js b/assets/js/hooks/virtualized_lines.js index 3fb936bb5..c4ad25bb6 100644 --- a/assets/js/hooks/virtualized_lines.js +++ b/assets/js/hooks/virtualized_lines.js @@ -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() { diff --git a/assets/js/lib/attribute.js b/assets/js/lib/attribute.js index ddec0a63e..b85c94927 100644 --- a/assets/js/lib/attribute.js +++ b/assets/js/lib/attribute.js @@ -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(""); } diff --git a/lib/livebook_web/components/core_components.ex b/lib/livebook_web/components/core_components.ex index 02d8de92f..2be466bae 100644 --- a/lib/livebook_web/components/core_components.ex +++ b/lib/livebook_web/components/core_components.ex @@ -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)} >
<%= @source %>
""" @@ -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 + +
+
+ + """ + 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 diff --git a/lib/livebook_web/live/js_view_component.ex b/lib/livebook_web/live/js_view_component.ex index e853d3cf9..9d8c7f692 100644 --- a/lib/livebook_web/live/js_view_component.ex +++ b/lib/livebook_web/live/js_view_component.ex @@ -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)} > """ diff --git a/lib/livebook_web/live/output/audio_input_component.ex b/lib/livebook_web/live/output/audio_input_component.ex index 4fe7a4042..746ff5adb 100644 --- a/lib/livebook_web/live/output/audio_input_component.ex +++ b/lib/livebook_web/live/output/audio_input_component.ex @@ -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)} >