mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-11 14:06:20 +08:00
Mobile quality-of-life (part 1) (#2013)
This commit is contained in:
parent
4899767673
commit
e1ac442c8c
10 changed files with 130 additions and 50 deletions
|
@ -166,6 +166,15 @@ Also some spacing adjustments.
|
|||
.doctest-status-decoration-failed {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
--decoration-size: 10px;
|
||||
}
|
||||
|
||||
@media screen(md) {
|
||||
.doctest-status-decoration-running,
|
||||
.doctest-status-decoration-success,
|
||||
.doctest-status-decoration-failed {
|
||||
--decoration-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.doctest-status-decoration-running::after,
|
||||
|
@ -175,8 +184,8 @@ Also some spacing adjustments.
|
|||
border-radius: 2px;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
height: var(--decoration-size);
|
||||
width: var(--decoration-size);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
|
@ -16,7 +16,11 @@ import { morphdomOptions } from "./dom";
|
|||
import { loadUserData } from "./lib/user";
|
||||
import { loadAppAuthToken } from "./lib/app";
|
||||
import { settingsStore } from "./lib/settings";
|
||||
import { registerTopbar, registerGlobalEventHandlers } from "./events";
|
||||
import {
|
||||
registerTopbar,
|
||||
registerGlobalEventHandlers,
|
||||
disableZoomOnInputFocus,
|
||||
} from "./events";
|
||||
import { cookieOptions } from "./lib/utils";
|
||||
import {
|
||||
loadConfirmOptOutIds,
|
||||
|
@ -54,6 +58,8 @@ function connect() {
|
|||
|
||||
registerGlobalEventHandlersForConfirm();
|
||||
|
||||
disableZoomOnInputFocus();
|
||||
|
||||
// Reflect global configuration in attributes to enable CSS rules
|
||||
settingsStore.getAndSubscribe((settings) => {
|
||||
document.body.setAttribute("data-editor-theme", settings.editor_theme);
|
||||
|
|
|
@ -98,3 +98,27 @@ export function registerGlobalEventHandlers() {
|
|||
{ capture: true }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the auto-zoom behavior when focusing an input on a touch device.
|
||||
*
|
||||
* It is important that this should not prevent users from manually
|
||||
* zooming if they wish. There isn't a portable solution to this
|
||||
* problem, so this hook is a no-op if the detected device is not
|
||||
* known to behave well.
|
||||
*
|
||||
* See: https://stackoverflow.com/questions/2989263/disable-auto-zoom-in-input-text-tag-safari-on-iphone
|
||||
*/
|
||||
export function disableZoomOnInputFocus() {
|
||||
const isWebKit = /AppleWebKit/.test(navigator.userAgent);
|
||||
const isTouchScreen =
|
||||
"ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
if (isWebKit && isTouchScreen) {
|
||||
const viewportTag = document.querySelector("meta[name='viewport']");
|
||||
|
||||
if (viewportTag) {
|
||||
viewportTag.content += ", maximum-scale=1.0";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,14 @@ const Cell = {
|
|||
`cells:${this.props.cellId}`,
|
||||
(event) => this.handleCellEvent(event)
|
||||
);
|
||||
|
||||
// DOM events
|
||||
|
||||
this._handleViewportResize = this.handleViewportResize.bind(this);
|
||||
window.visualViewport.addEventListener(
|
||||
"resize",
|
||||
this._handleViewportResize
|
||||
);
|
||||
},
|
||||
|
||||
disconnected() {
|
||||
|
@ -99,6 +107,11 @@ const Cell = {
|
|||
this.unsubscribeFromNavigationEvents();
|
||||
this.unsubscribeFromCellsEvents();
|
||||
this.unsubscribeFromCellEvents();
|
||||
|
||||
window.visualViewport.removeEventListener(
|
||||
"resize",
|
||||
this._handleViewportResize
|
||||
);
|
||||
},
|
||||
|
||||
updated() {
|
||||
|
@ -266,6 +279,12 @@ const Cell = {
|
|||
delete this.liveEditors[tag];
|
||||
},
|
||||
|
||||
handleViewportResize() {
|
||||
if (this.isFocused) {
|
||||
this.scrollActiveElementIntoView();
|
||||
}
|
||||
},
|
||||
|
||||
currentEditor() {
|
||||
return this.liveEditors[this.currentEditorTag()];
|
||||
},
|
||||
|
@ -325,13 +344,7 @@ const Cell = {
|
|||
// sets new cursor position. To achieve this, we simply put this task
|
||||
// at the end of event loop, ensuring the editor mousedown handler is
|
||||
// executed first
|
||||
setTimeout(() => {
|
||||
scrollIntoView(document.activeElement, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, 0);
|
||||
setTimeout(this.scrollActiveElementIntoView.bind(this), 0);
|
||||
|
||||
this.broadcastSelection();
|
||||
}
|
||||
|
@ -407,6 +420,14 @@ const Cell = {
|
|||
});
|
||||
}
|
||||
},
|
||||
|
||||
scrollActiveElementIntoView() {
|
||||
scrollIntoView(document.activeElement, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Cell;
|
||||
|
|
|
@ -310,6 +310,8 @@ class LiveEditor {
|
|||
: "off",
|
||||
});
|
||||
|
||||
this._setScreenDependantEditorOptions();
|
||||
|
||||
this.editor.addAction({
|
||||
contextMenuGroupId: "word-wrapping",
|
||||
id: "enable-word-wrapping",
|
||||
|
@ -333,6 +335,7 @@ class LiveEditor {
|
|||
entries.forEach((entry) => {
|
||||
// Ignore hidden container.
|
||||
if (this.container.offsetHeight > 0) {
|
||||
this._setScreenDependantEditorOptions();
|
||||
this.editor.layout();
|
||||
}
|
||||
});
|
||||
|
@ -366,6 +369,26 @@ class LiveEditor {
|
|||
this._initializeWidgets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Monaco editor options that depend on the current screen's size.
|
||||
*/
|
||||
_setScreenDependantEditorOptions() {
|
||||
if (window.screen.width < 768) {
|
||||
this.editor.updateOptions({
|
||||
folding: false,
|
||||
lineDecorationsWidth: 16,
|
||||
lineNumbersMinChars:
|
||||
Math.floor(Math.log10(this.editor.getModel().getLineCount())) + 3,
|
||||
});
|
||||
} else {
|
||||
this.editor.updateOptions({
|
||||
folding: true,
|
||||
lineDecorationsWidth: 10,
|
||||
lineNumbersMinChars: 5,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines cell-specific providers for various editor features.
|
||||
*/
|
||||
|
|
|
@ -95,6 +95,7 @@ class DetailsWidget {
|
|||
"editor-theme-aware-ansi"
|
||||
);
|
||||
detailsNode.style.fontSize = `${fontSize}px`;
|
||||
detailsNode.style.lineHeight = `${lineHeight}px`;
|
||||
|
||||
this._overlayWidget = {
|
||||
getId: () => `livebook.doctest.overlay.${line}`,
|
||||
|
|
|
@ -70,7 +70,7 @@ import "monaco-editor/esm/vs/editor/contrib/readOnlyMessage/browser/contribution
|
|||
// import 'vs/base/browser/ui/codicons/codiconStyles';
|
||||
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard";
|
||||
// import "monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess";
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="flex flex-col relative"
|
||||
class="flex flex-col relative scroll-mt-[50px] sm:scroll-mt-0"
|
||||
data-el-cell
|
||||
id={"cell-#{@cell_view.id}"}
|
||||
phx-hook="Cell"
|
||||
|
@ -268,7 +268,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<%= render_slot(@primary) %>
|
||||
</div>
|
||||
<div
|
||||
class="relative z-20 flex items-center justify-end space-x-2"
|
||||
class="relative z-20 flex items-center justify-end md:space-x-2"
|
||||
role="toolbar"
|
||||
aria-label="cell actions"
|
||||
data-el-actions
|
||||
|
|
|
@ -14,43 +14,39 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
data-el-insert-buttons
|
||||
>
|
||||
<div class={
|
||||
"w-full absolute z-10 hover:z-[11] #{if(@persistent, do: "opacity-100", else: "opacity-0")} hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center"
|
||||
"w-full md:absolute z-10 hover:z-[11] #{if(@persistent, do: "opacity-100", else: "opacity-0")} hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center"
|
||||
}>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-1 top-0 bottom-0 flex items-center transform -translate-x-full">
|
||||
<.menu id={"cell-#{@id}-insert"} position={:bottom_left} distant>
|
||||
<:toggle>
|
||||
<button class="button-base button-small flex items-center pr-1">
|
||||
<div
|
||||
class="pr-2"
|
||||
phx-click="insert_cell_below"
|
||||
phx-value-type="code"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-cell_id={@cell_id}
|
||||
phx-value-language="elixir"
|
||||
>
|
||||
+ <%= @default_language |> Atom.to_string() |> String.capitalize() %>
|
||||
</div>
|
||||
<div class="pl-1 flex items-center border-l border-gray-200">
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none" />
|
||||
</div>
|
||||
</button>
|
||||
</:toggle>
|
||||
<.menu_item>
|
||||
<button role="menuitem" phx-click="set_default_language" phx-value-language="elixir">
|
||||
<.cell_icon cell_type={:code} language={:elixir} />
|
||||
<span>Elixir</span>
|
||||
</button>
|
||||
</.menu_item>
|
||||
<.menu_item>
|
||||
<button role="menuitem" phx-click="set_default_language" phx-value-language="erlang">
|
||||
<.cell_icon cell_type={:code} language={:erlang} />
|
||||
<span>Erlang</span>
|
||||
</button>
|
||||
</.menu_item>
|
||||
</.menu>
|
||||
</div>
|
||||
</div>
|
||||
<.menu id={"cell-#{@id}-insert"} position={:bottom_left} distant>
|
||||
<:toggle>
|
||||
<button class="button-base button-small flex items-center pr-1">
|
||||
<div
|
||||
class="pr-2"
|
||||
phx-click="insert_cell_below"
|
||||
phx-value-type="code"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-cell_id={@cell_id}
|
||||
phx-value-language="elixir"
|
||||
>
|
||||
+ <%= @default_language |> Atom.to_string() |> String.capitalize() %>
|
||||
</div>
|
||||
<div class="pl-1 flex items-center border-l border-gray-200">
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none" />
|
||||
</div>
|
||||
</button>
|
||||
</:toggle>
|
||||
<.menu_item>
|
||||
<button role="menuitem" phx-click="set_default_language" phx-value-language="elixir">
|
||||
<.cell_icon cell_type={:code} language={:elixir} />
|
||||
<span>Elixir</span>
|
||||
</button>
|
||||
</.menu_item>
|
||||
<.menu_item>
|
||||
<button role="menuitem" phx-click="set_default_language" phx-value-language="erlang">
|
||||
<.cell_icon cell_type={:code} language={:erlang} />
|
||||
<span>Erlang</span>
|
||||
</button>
|
||||
</.menu_item>
|
||||
</.menu>
|
||||
<.menu id={"#{@id}-block-menu"} position={:bottom_left}>
|
||||
<:toggle>
|
||||
<button class="button-base button-small">+ Block</button>
|
||||
|
|
|
@ -42,7 +42,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
</button>
|
||||
</div>
|
||||
<h2
|
||||
class="grow text-gray-800 font-semibold text-2xl px-1 -ml-1.5 rounded-lg border border-transparent whitespace-pre-wrap cursor-text"
|
||||
class="grow text-gray-800 font-semibold text-2xl px-1 -ml-1.5 rounded-lg border border-transparent whitespace-pre-wrap cursor-text scroll-mt-[50px] sm:scroll-mt-0"
|
||||
tabindex="0"
|
||||
id={@section_view.html_id}
|
||||
data-el-heading
|
||||
|
|
Loading…
Add table
Reference in a new issue