mirror of
https://github.com/codex-team/editor.js
synced 2024-06-08 00:42:31 +02:00
fix(ui): Prevent scrolling top when opening toolbox on mobile (#2034)
* Fix scrolling issue on mobile when locking scroll on body * Simplify * Fix typo * Add popup example * Update changelog * update popup example page * Use hard scroll lock only for ios * Update version in changelog * Remove unused css class name Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
parent
96c0bcb573
commit
8ae8823dcd
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
### 2.24.2
|
||||
|
||||
- `Fix` — Scrolling issue when opening toolbox on mobile fixed
|
||||
- `Fix` — Typo in toolbox empty placeholder fixed
|
||||
- `Improvement` — *Dev Example Page* - Add popup example page
|
||||
|
||||
### 2.24.1
|
||||
|
||||
— `Fix` — The I18n of Tools` titles at the Toolbox now works correctly [#2030](https://github.com/codex-team/editor.js/issues/2030)
|
||||
|
|
|
@ -270,6 +270,56 @@ body {
|
|||
padding: 30px;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Styles for the popup example page
|
||||
*/
|
||||
.ce-example--popup {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ce-example--popup .ce-example__content {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.ce-example-popup__overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #00000085;
|
||||
}
|
||||
|
||||
.ce-example-popup__popup {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media all and (max-width: 730px){
|
||||
.ce-example-popup__popup {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
transform: none;
|
||||
max-height: none;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.show-block-boundaries .ce-block {
|
||||
box-shadow: inset 0 0 0 1px #eff2f5;
|
||||
}
|
||||
|
|
|
@ -195,7 +195,7 @@
|
|||
"toolbox": {
|
||||
"Add": "Добавить",
|
||||
"Filter": "Поиск",
|
||||
"Noting found": "Ничего не найдено"
|
||||
"Nothing found": "Ничего не найдено"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
131
example/example-popup.html
Normal file
131
example/example-popup.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
|
||||
<!--
|
||||
Use this page for debugging purposes.
|
||||
|
||||
This page can be used for testing editor nested in a popup.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Editor.js 🤩🧦🤨 example: Popup</title>
|
||||
<link href="assets/demo.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="ce-example ce-example--popup">
|
||||
<div class="ce-example__header">
|
||||
<a class="ce-example__header-logo" href="https://codex.so/editor">Editor.js 🤩🧦🤨</a>
|
||||
|
||||
<div class="ce-example__header-menu">
|
||||
<a href="https://github.com/editor-js" target="_blank">Plugins</a>
|
||||
<a href="https://editorjs.io/usage" target="_blank">Usage</a>
|
||||
<a href="https://editorjs.io/configuration" target="_blank">Configuration</a>
|
||||
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ce-example__content ce-example__content--with-bg _ce-example__content--small">
|
||||
<div id="hint-core" style="text-align: center; padding-top: 20px">
|
||||
No core bundle file found. Run <code class="inline-code">yarn build</code>
|
||||
</div>
|
||||
<div class="stub">
|
||||
<h1>Base concepts</h1>
|
||||
<p>
|
||||
Editor.js is a block-style editor for rich media stories. It outputs clean data in JSON instead of heavy HTML markup. And more important thing is that Editor.js is designed to be API extendable and pluggable.
|
||||
</p>
|
||||
<p>
|
||||
So there are a few key features:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Clean data output</li>
|
||||
<li>API pluggable</li>
|
||||
<li>Open source</li>
|
||||
</ul>
|
||||
<h2>
|
||||
What does it mean block-styled
|
||||
</h2>
|
||||
<p>
|
||||
In other editors, the workspace is provided by single contenteditable element in where you can create different HTML markup. All of us saw permanent bugs with moving text fragments or scaling images, while page parts are jumping and twitches. Or highlighting big parts of the text in the case when you just want to make few words to be a heading or bold.
|
||||
</p>
|
||||
<p>
|
||||
The Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core.
|
||||
</p>
|
||||
<p>
|
||||
At the same time, most useful features as arrow-navigation, copy & paste, cross block selection, and others works almost as in the familiar editors.
|
||||
</p>
|
||||
<h2>
|
||||
What is clean data
|
||||
</h2>
|
||||
<p>
|
||||
But the more interesting thing is, as mentioned above, that Editor.js returns clean data instead of HTML-markup. Take a look at the example.
|
||||
</p>
|
||||
<p>
|
||||
If our entry consists of few paragraphs and a heading, in popular Medium editor after saving we will have something like this:
|
||||
</p>
|
||||
<p>
|
||||
As you can see, there are only data we need: a list of structural Blocks with their content description.
|
||||
</p>
|
||||
<p>
|
||||
You can use this data to easily render in Web, native mobile/desktop application, pass to Audio Readers, create templates for Facebook Instant Articles, AMP, RSS, create chat-bots, and many others.
|
||||
</p>
|
||||
<p>
|
||||
Also, the clean data can be useful for backend processing: sanitizing, validation, injecting an advertising or other stuff, extracting Headings, make covers for social networks from Image Blocks, and other.
|
||||
</p>
|
||||
<h2>
|
||||
API pluggable?
|
||||
</h2>
|
||||
<p>
|
||||
A key value of the Editor is the API. All main functional units of the editor — Blocks, Inline Formatting Tools, Block Tunes — are provided by external plugins that use Editor's API.
|
||||
</p>
|
||||
<p>
|
||||
We decide to extract all these Tools to separate scripts to make Editor's Core more abstract and make API more powerful. Any challenges and tasks you are facing can be implemented by your own plugins using the API.
|
||||
</p>
|
||||
<p>
|
||||
At the same time, API is created to be easy-to-understand and simple-to-use.
|
||||
</p>
|
||||
<h2>
|
||||
Open Source, so?
|
||||
</h2>
|
||||
<p>
|
||||
Editor.js is more than just an editor. It is a big open-source community of developers and contributors. Anyone can suggest an improvement or a bug fix. Anyone can create new cool API features and plugins.
|
||||
</p>
|
||||
<p>
|
||||
We will support each developer of Editor.js plugins: the best solutions will be collected to the Awesome List and promoted to the community. Together we can create a big suite of different Blocks, Inline Tools, Block Tunes that can hit a wide specter of tasks.
|
||||
</p>
|
||||
<p>
|
||||
Thanks for your interest. Hope you enjoy Editor.js.
|
||||
</p>
|
||||
</div>
|
||||
<div class="ce-example-popup">
|
||||
<div class="ce-example-popup__overlay"></div>
|
||||
<div class="ce-example-popup__popup">
|
||||
<div id="editorjs"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ce-example__output">
|
||||
<div class="ce-example__output-footer">
|
||||
<a href="https://codex.so" style="font-weight: bold;">Made by CodeX</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load Editor.js's Core -->
|
||||
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
||||
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
|
||||
|
||||
<!-- Initialization -->
|
||||
<script>
|
||||
var editor1 = new EditorJS({
|
||||
holder: 'editorjs',
|
||||
tools: {
|
||||
header: {
|
||||
class: Header,
|
||||
shortcut: 'CMD+SHIFT+H'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -15,7 +15,7 @@
|
|||
"toolbox": {
|
||||
"Add": "",
|
||||
"Filter": "",
|
||||
"Noting found": ""
|
||||
"Nothing found": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -409,7 +409,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
tools: this.Editor.Tools.blockTools,
|
||||
i18nLabels: {
|
||||
filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'),
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Noting found'),
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Nothing found'),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -769,3 +769,13 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]
|
|||
export function isMobileScreen(): boolean {
|
||||
return window.matchMedia('(max-width: 650px)').matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if current device runs iOS
|
||||
*/
|
||||
export const isIosDevice =
|
||||
typeof window !== 'undefined' &&
|
||||
window.navigator &&
|
||||
window.navigator.platform &&
|
||||
(/iP(ad|hone|od)/.test(window.navigator.platform) ||
|
||||
(window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1));
|
|
@ -4,6 +4,7 @@ import Flipper from '../flipper';
|
|||
import SearchInput from './search-input';
|
||||
import EventsDispatcher from './events';
|
||||
import { isMobileScreen, keyCodes, cacheable } from '../utils';
|
||||
import ScrollLocker from './scroll-locker';
|
||||
|
||||
/**
|
||||
* Describe parameters for rendering the single item of Popover
|
||||
|
@ -126,7 +127,6 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
noFoundMessageShown: string;
|
||||
popoverOverlay: string;
|
||||
popoverOverlayHidden: string;
|
||||
documentScrollLocked: string;
|
||||
} {
|
||||
return {
|
||||
popover: 'ce-popover',
|
||||
|
@ -142,10 +142,14 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
noFoundMessageShown: 'ce-popover__no-found--shown',
|
||||
popoverOverlay: 'ce-popover__overlay',
|
||||
popoverOverlayHidden: 'ce-popover__overlay--hidden',
|
||||
documentScrollLocked: 'ce-scroll-locked',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ScrollLocker instance
|
||||
*/
|
||||
private scrollLocker = new ScrollLocker()
|
||||
|
||||
/**
|
||||
* Creates the Popover
|
||||
*
|
||||
|
@ -197,7 +201,7 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
}
|
||||
|
||||
if (isMobileScreen()) {
|
||||
document.documentElement.classList.add(Popover.CSS.documentScrollLocked);
|
||||
this.scrollLocker.lock();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,7 +215,7 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
this.flipper.deactivate();
|
||||
|
||||
if (isMobileScreen()) {
|
||||
document.documentElement.classList.remove(Popover.CSS.documentScrollLocked);
|
||||
this.scrollLocker.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
64
src/components/utils/scroll-locker.ts
Normal file
64
src/components/utils/scroll-locker.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { isIosDevice } from '../utils';
|
||||
|
||||
/**
|
||||
* Utility allowing to lock body scroll on demand
|
||||
*/
|
||||
export default class ScrollLocker {
|
||||
/**
|
||||
* Style classes
|
||||
*/
|
||||
private static CSS = {
|
||||
scrollLocked: 'ce-scroll-locked',
|
||||
scrollLockedHard: 'ce-scroll-locked--hard',
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores scroll position, used for hard scroll lock
|
||||
*/
|
||||
private scrollPosition: null|number
|
||||
|
||||
/**
|
||||
* Locks body element scroll
|
||||
*/
|
||||
public lock(): void {
|
||||
if (isIosDevice) {
|
||||
this.lockHard();
|
||||
} else {
|
||||
document.body.classList.add(ScrollLocker.CSS.scrollLocked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks body element scroll
|
||||
*/
|
||||
public unlock(): void {
|
||||
if (isIosDevice) {
|
||||
this.unlockHard();
|
||||
} else {
|
||||
document.body.classList.remove(ScrollLocker.CSS.scrollLocked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks scroll in a hard way (via setting fixed position to body element)
|
||||
*/
|
||||
private lockHard(): void {
|
||||
this.scrollPosition = window.pageYOffset;
|
||||
document.documentElement.style.setProperty(
|
||||
'--window-scroll-offset',
|
||||
`${this.scrollPosition}px`
|
||||
);
|
||||
document.body.classList.add(ScrollLocker.CSS.scrollLockedHard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks hard scroll lock
|
||||
*/
|
||||
private unlockHard(): void {
|
||||
document.body.classList.remove(ScrollLocker.CSS.scrollLockedHard);
|
||||
if (this.scrollPosition !== null) {
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
}
|
||||
this.scrollPosition = null;
|
||||
}
|
||||
}
|
|
@ -128,11 +128,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ce-scroll-locked, .ce-scroll-locked > body {
|
||||
height: 100vh;
|
||||
.ce-scroll-locked {
|
||||
overflow: hidden;
|
||||
/**
|
||||
* Mobile Safari fix
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ce-scroll-locked--hard {
|
||||
overflow: hidden;
|
||||
top: calc(-1 * var(--window-scroll-offset));
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
Loading…
Reference in a new issue