Feature: mobile ux improvements (#802)
* Inline Toolbar now works on mobile devices * Some styles updates * Inline Toolbar now detects left and rigth sides of content * listen selection change only on touch devices * improve toolbar paddings * invalidate content rectangle cache on resize * Toolbar now works on mobile devices * prod bundle * add changelog * upd texts * do not hide toolbar on window resize * rm sever
8
dist/editor.js
vendored
12
dist/sprite.svg
vendored
|
|
@ -8,9 +8,7 @@
|
|||
<path d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
|
||||
|
||||
</symbol>
|
||||
<symbol id="bold" viewBox="0 0 13 15">
|
||||
<path d="M5.996 13.9H1.752c-.613 0-1.05-.137-1.312-.412-.262-.275-.393-.712-.393-1.312V1.737C.047 1.125.18.684.449.416.718.147 1.152.013 1.752.013h4.5a10.5 10.5 0 0 1 1.723.123c.487.082.922.24 1.308.474a3.43 3.43 0 0 1 1.449 1.738c.132.363.199.747.199 1.151 0 1.39-.695 2.406-2.084 3.05 1.825.581 2.737 1.712 2.737 3.391 0 .777-.199 1.477-.596 2.099a3.581 3.581 0 0 1-1.61 1.378c-.424.177-.91.301-1.46.374-.549.073-1.19.109-1.922.109zm-.209-6.167H2.86v4.055h3.022c1.9 0 2.851-.686 2.851-2.056 0-.7-.246-1.21-.739-1.525-.492-.316-1.228-.474-2.207-.474zM2.86 2.125v3.59h2.577c.7 0 1.242-.066 1.624-.198a1.55 1.55 0 0 0 .876-.758c.158-.265.237-.562.237-.89 0-.702-.25-1.167-.748-1.398-.499-.23-1.26-.346-2.283-.346H2.86z"/>
|
||||
|
||||
<symbol id="bold"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/>
|
||||
</symbol>
|
||||
<symbol id="cross" viewBox="0 0 237 237">
|
||||
<path transform="rotate(45 280.675 51.325)" d="M191 191V73c0-5.523 4.477-10 10-10h25c5.523 0 10 4.477 10 10v118h118c5.523 0 10 4.477 10 10v25c0 5.523-4.477 10-10 10H236v118c0 5.523-4.477 10-10 10h-25c-5.523 0-10-4.477-10-10V236H73c-5.523 0-10-4.477-10-10v-25c0-5.523 4.477-10 10-10h118z"/>
|
||||
|
|
@ -24,13 +22,11 @@
|
|||
</g>
|
||||
|
||||
</symbol>
|
||||
<symbol id="italic" viewBox="0 0 6 15">
|
||||
<path d="M4 5.2l-1.368 7.474c-.095.518-.29.91-.585 1.175a1.468 1.468 0 0 1-1.01.398c-.379 0-.662-.136-.85-.407-.186-.272-.234-.66-.141-1.166L1.4 5.276c.093-.511.282-.896.567-1.155a1.43 1.43 0 0 1 .994-.389c.38 0 .668.13.867.389.199.259.256.618.172 1.08zm-.79-2.67c-.36 0-.648-.111-.863-.332-.215-.221-.286-.534-.212-.938.067-.366.253-.668.559-.905A1.57 1.57 0 0 1 3.673 0c.334 0 .612.107.831.322.22.215.292.527.217.938-.073.398-.256.709-.55.933a1.55 1.55 0 0 1-.961.336z"/>
|
||||
<symbol id="italic">
|
||||
<path d="M19.211 15.326l-1.44 7.108c-.1.493-.305.865-.615 1.117a1.64 1.64 0 0 1-1.064.379c-.4 0-.697-.13-.894-.388-.197-.258-.247-.627-.15-1.108l1.426-7.036c.098-.486.297-.853.597-1.1.299-.245.648-.368 1.047-.368.399 0 .703.123.912.369.21.246.27.588.181 1.027zm-.831-2.663c-.38 0-.682-.116-.909-.35-.227-.232-.301-.561-.223-.987.07-.385.266-.703.588-.952.322-.25.665-.374 1.03-.374.353 0 .645.113.876.34.232.225.308.554.229.986-.077.42-.27.747-.58.983-.308.236-.646.354-1.011.354z"/>
|
||||
|
||||
</symbol>
|
||||
<symbol id="link" viewBox="0 0 15 14">
|
||||
<path transform="rotate(-45 11.83 6.678)" d="M11.332 4.013a51.07 51.07 0 0 1-2.28.001A1.402 1.402 0 0 0 7.7 2.25H3.65a1.4 1.4 0 1 0 0 2.8h.848c.206.86.693 1.61 1.463 2.25H3.65a3.65 3.65 0 1 1 0-7.3H7.7a3.65 3.65 0 0 1 3.632 4.013zM10.9 0h2a3.65 3.65 0 0 1 0 7.3H8.85a3.65 3.65 0 0 1-3.632-4.011A62.68 62.68 0 0 1 7.5 3.273 1.401 1.401 0 0 0 8.85 5.05h4.05a1.4 1.4 0 0 0 0-2.8h-.48C12.274 1.664 11.694.785 10.9 0z"/>
|
||||
|
||||
<symbol id="link"><path d="M15.439 21.153a4.202 4.202 0 0 0 2.72.63l-.985.986a4.202 4.202 0 1 1-5.943-5.945l2.093-2.093a4.202 4.202 0 0 1 5.934-.009l-1.655 1.656a5.886 5.886 0 0 1-.02.019 1.835 1.835 0 0 0-2.585.009l-2.093 2.093a1.836 1.836 0 0 0 2.534 2.654zm3.122-8.306a4.202 4.202 0 0 0-2.72-.63l.985-.986a4.202 4.202 0 1 1 5.943 5.945l-2.093 2.093a4.202 4.202 0 0 1-5.934.009l1.655-1.656.02-.019a1.835 1.835 0 0 0 2.585-.009l2.093-2.093a1.836 1.836 0 0 0-2.534-2.654z"/>
|
||||
</symbol>
|
||||
<symbol id="plus" viewBox="0 0 14 14">
|
||||
<path d="M8.05 5.8h4.625a1.125 1.125 0 0 1 0 2.25H8.05v4.625a1.125 1.125 0 0 1-2.25 0V8.05H1.125a1.125 1.125 0 0 1 0-2.25H5.8V1.125a1.125 1.125 0 0 1 2.25 0V5.8z"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
### 2.15
|
||||
|
||||
- `Improvements` — Inline Toolbar now works on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)
|
||||
- `Improvements` — Toolbar looks better on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)
|
||||
|
||||
### 2.14
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
<svg width="13" height="15" viewBox="0 0 13 15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M5.996 13.9H1.752c-.613 0-1.05-.137-1.312-.412-.262-.275-.393-.712-.393-1.312V1.737C.047 1.125.18.684.449.416.718.147 1.152.013 1.752.013h4.5a10.5 10.5 0 0 1 1.723.123c.487.082.922.24 1.308.474a3.43 3.43 0 0 1 1.449 1.738c.132.363.199.747.199 1.151 0 1.39-.695 2.406-2.084 3.05 1.825.581 2.737 1.712 2.737 3.391 0 .777-.199 1.477-.596 2.099a3.581 3.581 0 0 1-1.61 1.378c-.424.177-.91.301-1.46.374-.549.073-1.19.109-1.922.109zm-.209-6.167H2.86v4.055h3.022c1.9 0 2.851-.686 2.851-2.056 0-.7-.246-1.21-.739-1.525-.492-.316-1.228-.474-2.207-.474zM2.86 2.125v3.59h2.577c.7 0 1.242-.066 1.624-.198a1.55 1.55 0 0 0 .876-.758c.158-.265.237-.562.237-.89 0-.702-.25-1.167-.748-1.398-.499-.23-1.26-.346-2.283-.346H2.86z"/>
|
||||
</svg>
|
||||
<svg width="12" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 857 B After Width: | Height: | Size: 774 B |
|
|
@ -1,3 +1,3 @@
|
|||
<svg width="6" height="15" viewBox="0 0 6 15" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 5.2l-1.368 7.474c-.095.518-.29.91-.585 1.175a1.468 1.468 0 0 1-1.01.398c-.379 0-.662-.136-.85-.407-.186-.272-.234-.66-.141-1.166L1.4 5.276c.093-.511.282-.896.567-1.155a1.43 1.43 0 0 1 .994-.389c.38 0 .668.13.867.389.199.259.256.618.172 1.08zm-.79-2.67c-.36 0-.648-.111-.863-.332-.215-.221-.286-.534-.212-.938.067-.366.253-.668.559-.905A1.57 1.57 0 0 1 3.673 0c.334 0 .612.107.831.322.22.215.292.527.217.938-.073.398-.256.709-.55.933a1.55 1.55 0 0 1-.961.336z" />
|
||||
<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.211 15.326l-1.44 7.108c-.1.493-.305.865-.615 1.117a1.64 1.64 0 0 1-1.064.379c-.4 0-.697-.13-.894-.388-.197-.258-.247-.627-.15-1.108l1.426-7.036c.098-.486.297-.853.597-1.1.299-.245.648-.368 1.047-.368.399 0 .703.123.912.369.21.246.27.588.181 1.027zm-.831-2.663c-.38 0-.682-.116-.909-.35-.227-.232-.301-.561-.223-.987.07-.385.266-.703.588-.952.322-.25.665-.374 1.03-.374.353 0 .645.113.876.34.232.225.308.554.229.986-.077.42-.27.747-.58.983-.308.236-.646.354-1.011.354z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 557 B |
|
|
@ -1,3 +1 @@
|
|||
<svg width="15" height="14" viewBox="0 0 15 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path transform="rotate(-45 11.83 6.678)" d="M11.332 4.013a51.07 51.07 0 0 1-2.28.001A1.402 1.402 0 0 0 7.7 2.25H3.65a1.4 1.4 0 1 0 0 2.8h.848c.206.86.693 1.61 1.463 2.25H3.65a3.65 3.65 0 1 1 0-7.3H7.7a3.65 3.65 0 0 1 3.632 4.013zM10.9 0h2a3.65 3.65 0 0 1 0 7.3H8.85a3.65 3.65 0 0 1-3.632-4.011A62.68 62.68 0 0 1 7.5 3.273 1.401 1.401 0 0 0 8.85 5.05h4.05a1.4 1.4 0 0 0 0-2.8h-.48C12.274 1.664 11.694.785 10.9 0z"/>
|
||||
</svg>
|
||||
<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg"><path d="M15.439 21.153a4.202 4.202 0 0 0 2.72.63l-.985.986a4.202 4.202 0 1 1-5.943-5.945l2.093-2.093a4.202 4.202 0 0 1 5.934-.009l-1.655 1.656a5.886 5.886 0 0 1-.02.019 1.835 1.835 0 0 0-2.585.009l-2.093 2.093a1.836 1.836 0 0 0 2.534 2.654zm3.122-8.306a4.202 4.202 0 0 0-2.72-.63l.985-.986a4.202 4.202 0 1 1 5.943 5.945l-2.093 2.093a4.202 4.202 0 0 1-5.934.009l1.655-1.656.02-.019a1.835 1.835 0 0 0 2.585-.009l2.093-2.093a1.836 1.836 0 0 0-2.534-2.654z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 554 B After Width: | Height: | Size: 526 B |
|
|
@ -56,7 +56,7 @@ export default class BoldInlineTool implements InlineTool {
|
|||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.appendChild($.svg('bold', 13, 15));
|
||||
this.nodes.button.appendChild($.svg('bold', 12, 14));
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default class ItalicInlineTool implements InlineTool {
|
|||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.appendChild($.svg('italic', 6, 15));
|
||||
this.nodes.button.appendChild($.svg('italic', 34, 34));
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export default class LinkInlineTool implements InlineTool {
|
|||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.appendChild($.svg('link', 15, 14));
|
||||
this.nodes.button.appendChild($.svg('link', 34, 34));
|
||||
this.nodes.button.appendChild($.svg('unlink', 16, 18));
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ export interface ListenerData {
|
|||
handler: (event: Event) => void;
|
||||
|
||||
/**
|
||||
* Should event bubbling be used or not
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
|
||||
*/
|
||||
useCapture: boolean;
|
||||
options: boolean | AddEventListenerOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,19 +56,19 @@ export default class Listeners extends Module {
|
|||
* @param {EventTarget} element - DOM element that needs to be listened
|
||||
* @param {String} eventType - event type
|
||||
* @param {Function} handler - method that will be fired on event
|
||||
* @param {Boolean} useCapture - use event bubbling
|
||||
* @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
|
||||
*/
|
||||
public on(
|
||||
element: EventTarget,
|
||||
eventType: string,
|
||||
handler: (event: Event) => void,
|
||||
useCapture: boolean = false,
|
||||
options: boolean | AddEventListenerOptions = false,
|
||||
): void {
|
||||
const assignedEventData = {
|
||||
element,
|
||||
eventType,
|
||||
handler,
|
||||
useCapture,
|
||||
options,
|
||||
};
|
||||
|
||||
const alreadyExist = this.findOne(element, eventType, handler);
|
||||
|
|
@ -76,7 +76,7 @@ export default class Listeners extends Module {
|
|||
if (alreadyExist) { return; }
|
||||
|
||||
this.allListeners.push(assignedEventData);
|
||||
element.addEventListener(eventType, handler, useCapture);
|
||||
element.addEventListener(eventType, handler, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,13 +85,13 @@ export default class Listeners extends Module {
|
|||
* @param {EventTarget} element - DOM element that we removing listener
|
||||
* @param {String} eventType - event type
|
||||
* @param {Function} handler - remove handler, if element listens several handlers on the same event type
|
||||
* @param {Boolean} useCapture - use event bubbling
|
||||
* @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
|
||||
*/
|
||||
public off(
|
||||
element: EventTarget,
|
||||
eventType: string,
|
||||
handler: (event: Event) => void,
|
||||
useCapture: boolean = false,
|
||||
options: boolean | AddEventListenerOptions = false,
|
||||
): void {
|
||||
const existingListeners = this.findAll(element, eventType, handler);
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ export default class Listeners extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
element.removeEventListener(eventType, handler, useCapture);
|
||||
element.removeEventListener(eventType, handler, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -196,18 +196,27 @@ export default class Toolbar extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Toolbar Min Height as Block's height
|
||||
* Plus Button and Toolbox positioned at the center of the Toolbar
|
||||
*/
|
||||
const contentOffset = Math.floor(currentBlock.offsetHeight / 2);
|
||||
const { isMobile } = this.Editor.UI;
|
||||
const blockHeight = currentBlock.offsetHeight;
|
||||
let toolbarY = currentBlock.offsetTop;
|
||||
|
||||
/**
|
||||
* 1) On desktop — Toolbar at the top of Block, Plus/Toolbox moved the center of Block
|
||||
* 2) On mobile — Toolbar at the bottom of Block
|
||||
*/
|
||||
if (!isMobile) {
|
||||
const contentOffset = Math.floor(blockHeight / 2);
|
||||
|
||||
this.nodes.plusButton.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
|
||||
this.Editor.Toolbox.nodes.toolbox.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
|
||||
} else {
|
||||
toolbarY += blockHeight;
|
||||
}
|
||||
|
||||
this.nodes.plusButton.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
|
||||
this.Editor.Toolbox.nodes.toolbox.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
|
||||
/**
|
||||
* Move Toolbar to the Top coordinate of Block
|
||||
*/
|
||||
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(currentBlock.offsetTop)}px, 0)`;
|
||||
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export default class InlineToolbar extends Module {
|
|||
public CSS = {
|
||||
inlineToolbar: 'ce-inline-toolbar',
|
||||
inlineToolbarShowed: 'ce-inline-toolbar--showed',
|
||||
inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented',
|
||||
inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented',
|
||||
buttonsWrapper: 'ce-inline-toolbar__buttons',
|
||||
actionsWrapper: 'ce-inline-toolbar__actions',
|
||||
inlineToolButton: 'ce-inline-tool',
|
||||
|
|
@ -79,6 +81,12 @@ export default class InlineToolbar extends Module {
|
|||
*/
|
||||
private focusedButtonIndex: number = -1;
|
||||
|
||||
/**
|
||||
* Cache for Inline Toolbar width
|
||||
* @type {number}
|
||||
*/
|
||||
private width: number = 0;
|
||||
|
||||
/**
|
||||
* Inline Toolbar Tools
|
||||
*
|
||||
|
|
@ -129,6 +137,10 @@ export default class InlineToolbar extends Module {
|
|||
*/
|
||||
this.addTools();
|
||||
|
||||
/**
|
||||
* Recalculate initial width with all buttons
|
||||
*/
|
||||
this.recalculateWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -148,6 +160,7 @@ export default class InlineToolbar extends Module {
|
|||
|
||||
this.move();
|
||||
this.open();
|
||||
this.Editor.Toolbar.close();
|
||||
|
||||
/** Check Tools state for selected fragment */
|
||||
this.checkToolsState();
|
||||
|
|
@ -178,6 +191,19 @@ export default class InlineToolbar extends Module {
|
|||
newCoords.x += Math.floor(selectionRect.width / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing
|
||||
*/
|
||||
const realLeftCoord = newCoords.x - this.width / 2;
|
||||
const realRightCoord = newCoords.x + this.width / 2;
|
||||
|
||||
/**
|
||||
* By default, Inline Toolbar has top-corner at the center
|
||||
* We are adding a modifiers for to move corner to the left or right
|
||||
*/
|
||||
this.nodes.wrapper.classList.toggle(this.CSS.inlineToolbarLeftOriented, realLeftCoord < this.Editor.UI.contentRect.left);
|
||||
this.nodes.wrapper.classList.toggle(this.CSS.inlineToolbarRightOriented, realRightCoord > this.Editor.UI.contentRect.right);
|
||||
|
||||
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
|
||||
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
|
||||
}
|
||||
|
|
@ -366,6 +392,18 @@ export default class InlineToolbar extends Module {
|
|||
if (lastVisibleButton) {
|
||||
lastVisibleButton.classList.add(this.CSS.inlineToolButtonLast);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate width because some buttons can be hidden
|
||||
*/
|
||||
this.recalculateWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate inline toolbar width
|
||||
*/
|
||||
private recalculateWidth(): void {
|
||||
this.width = this.nodes.wrapper.offsetWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import $ from '../dom';
|
|||
import _ from '../utils';
|
||||
|
||||
import Selection from '../selection';
|
||||
import Block from '../block';
|
||||
|
||||
/**
|
||||
* @class
|
||||
|
|
@ -53,10 +54,37 @@ export default class UI extends Module {
|
|||
}
|
||||
|
||||
/**
|
||||
* Width of center column of Editor
|
||||
* @type {number}
|
||||
* Return Width of center column of Editor
|
||||
* @return {DOMRect}
|
||||
*/
|
||||
public contentWidth: number = 650;
|
||||
public get contentRect(): DOMRect {
|
||||
if (this.contentRectCache) {
|
||||
return this.contentRectCache;
|
||||
}
|
||||
|
||||
const someBlock = this.nodes.wrapper.querySelector(`.${Block.CSS.content}`);
|
||||
|
||||
/**
|
||||
* When Editor is not ready, there is no Blocks, so return the default value
|
||||
*/
|
||||
if (!someBlock) {
|
||||
return {
|
||||
width: 650,
|
||||
left: 0,
|
||||
right: 0,
|
||||
} as DOMRect;
|
||||
}
|
||||
|
||||
this.contentRectCache = someBlock.getBoundingClientRect() as DOMRect;
|
||||
|
||||
return this.contentRectCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag that became true on mobile viewport
|
||||
* @type {boolean}
|
||||
*/
|
||||
public isMobile: boolean = false;
|
||||
|
||||
/**
|
||||
* HTML Elements used for UI
|
||||
|
|
@ -67,6 +95,21 @@ export default class UI extends Module {
|
|||
redactor: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache for center column rectangle info
|
||||
* Invalidates on window resize
|
||||
* @type {DOMRect}
|
||||
*/
|
||||
private contentRectCache: DOMRect = undefined;
|
||||
|
||||
/**
|
||||
* Handle window resize only when it finished
|
||||
* @type {() => void}
|
||||
*/
|
||||
private resizeDebouncer: () => void = _.debounce(() => {
|
||||
this.windowResize();
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
* Adds loader to editor while content is not ready
|
||||
*/
|
||||
|
|
@ -88,8 +131,19 @@ export default class UI extends Module {
|
|||
* Making main interface
|
||||
*/
|
||||
public async prepare(): Promise<void> {
|
||||
/**
|
||||
* Detect mobile version
|
||||
*/
|
||||
this.checkIsMobile();
|
||||
|
||||
/**
|
||||
* Make main UI elements
|
||||
*/
|
||||
await this.make();
|
||||
|
||||
/**
|
||||
* Loader for rendering process
|
||||
*/
|
||||
this.addLoader();
|
||||
|
||||
/**
|
||||
|
|
@ -134,6 +188,13 @@ export default class UI extends Module {
|
|||
this.nodes.holder.innerHTML = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for mobile mode and cache a result
|
||||
*/
|
||||
private checkIsMobile() {
|
||||
this.isMobile = window.innerWidth < 650;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes Editor.js interface
|
||||
* @return {Promise<void>}
|
||||
|
|
@ -154,7 +215,7 @@ export default class UI extends Module {
|
|||
/**
|
||||
* If Editor has injected into the narrow container, enable Narrow Mode
|
||||
*/
|
||||
if (this.nodes.holder.offsetWidth < this.contentWidth) {
|
||||
if (this.nodes.holder.offsetWidth < this.contentRect.width) {
|
||||
this.nodes.wrapper.classList.add(this.CSS.editorWrapperNarrow);
|
||||
}
|
||||
|
||||
|
|
@ -202,6 +263,36 @@ export default class UI extends Module {
|
|||
);
|
||||
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
|
||||
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);
|
||||
|
||||
/**
|
||||
* Handle selection change on mobile devices for the Inline Toolbar support
|
||||
*/
|
||||
if (_.isTouchSupported()) {
|
||||
this.Editor.Listeners.on(document, 'selectionchange', (event) => {
|
||||
this.selectionChanged(event as Event);
|
||||
}, true);
|
||||
}
|
||||
|
||||
this.Editor.Listeners.on(window, 'resize', () => {
|
||||
this.resizeDebouncer();
|
||||
}, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize window handler
|
||||
*/
|
||||
private windowResize(): void {
|
||||
/**
|
||||
* Invalidate content zone size cached, because it may be changed
|
||||
*/
|
||||
this.contentRectCache = null;
|
||||
|
||||
/**
|
||||
* Detect mobile version
|
||||
*/
|
||||
this.checkIsMobile();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -380,8 +471,6 @@ export default class UI extends Module {
|
|||
* Do not fire check on clicks at the Inline Toolbar buttons
|
||||
*/
|
||||
const target = event.target as HTMLElement;
|
||||
const clickedOnInlineToolbarButton = target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`);
|
||||
|
||||
const clickedInsideOfEditor = this.nodes.holder.contains(target) || Selection.isAtEditor;
|
||||
|
||||
if (!clickedInsideOfEditor) {
|
||||
|
|
@ -396,11 +485,6 @@ export default class UI extends Module {
|
|||
this.Editor.Toolbar.close();
|
||||
this.Editor.BlockSelection.clearSelection();
|
||||
|
||||
} else if (!clickedOnInlineToolbarButton) {
|
||||
/**
|
||||
* Move inline toolbar to the focused Block
|
||||
*/
|
||||
this.Editor.InlineToolbar.handleShowingEvent(event);
|
||||
}
|
||||
|
||||
if (Selection.isAtEditor) {
|
||||
|
|
@ -517,7 +601,26 @@ export default class UI extends Module {
|
|||
}
|
||||
|
||||
/**
|
||||
* Append prebuilded sprite with SVG icons
|
||||
* Handle selection changes on mobile devices
|
||||
* Uses for showing the Inline Toolbar
|
||||
* @param {Event} event
|
||||
*/
|
||||
private selectionChanged(event: Event): void {
|
||||
const focusedElement = Selection.anchorElement as Element;
|
||||
|
||||
/**
|
||||
* Event can be fired on clicks at the Editor elements, for example, at the Inline Toolbar
|
||||
* We need to skip such firings
|
||||
*/
|
||||
if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.Editor.InlineToolbar.handleShowingEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append prebuilt sprite with SVG icons
|
||||
*/
|
||||
private appendSVGSprite(): void {
|
||||
const spriteHolder = $.make('div');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* TextRange interface fot IE9-
|
||||
*/
|
||||
import _ from './utils';
|
||||
import $ from './dom';
|
||||
|
||||
interface TextRange {
|
||||
boundingTop: number;
|
||||
|
|
@ -55,6 +56,30 @@ export default class SelectionUtils {
|
|||
return selection ? selection.anchorNode : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected anchor element
|
||||
* @return {Element|null}
|
||||
*/
|
||||
static get anchorElement(): Element | null {
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchorNode = selection.anchorNode;
|
||||
|
||||
if (!anchorNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$.isElement(anchorNode)) {
|
||||
return anchorNode.parentElement;
|
||||
} else {
|
||||
return anchorNode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selection offset according to the anchor node
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
|
||||
|
|
|
|||
|
|
@ -338,4 +338,17 @@ export default class Util {
|
|||
return Util.deepMerge(target, ...sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if current device supports touch events
|
||||
*
|
||||
* Note! This is a simple solution, it can give false-positive results.
|
||||
* To detect touch devices more carefully, use 'touchstart' event listener
|
||||
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
public static isTouchSupported(): boolean {
|
||||
return 'ontouchstart' in document.documentElement;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,3 +100,20 @@
|
|||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes panelShowing {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.9);
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
to {
|
||||
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@
|
|||
@apply --toolbar-button;
|
||||
|
||||
&:not(:nth-child(3n+3)) {
|
||||
margin-right: 5px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
&:nth-child(n+4) {
|
||||
margin-top: 5px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
&--active {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,36 @@
|
|||
.ce-inline-toolbar {
|
||||
@apply --overlay-pane;
|
||||
padding: 6px;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
padding: 3px;
|
||||
transform: translateX(-50%) translateY(8px) scale(0.9);
|
||||
box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
|
||||
5px -12px 34px -13px rgba(97, 105, 134, 0.6),
|
||||
0 26px 52px 3px rgba(147, 165, 186, 0.24);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform 150ms ease, opacity 250ms ease;
|
||||
will-change: transform, opacity;
|
||||
|
||||
&--showed {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%)
|
||||
}
|
||||
|
||||
&--left-oriented {
|
||||
transform: translateX(-23px) translateY(8px) scale(0.9);
|
||||
}
|
||||
|
||||
&--left-oriented&--showed {
|
||||
transform: translateX(-23px);
|
||||
}
|
||||
|
||||
&--right-oriented {
|
||||
transform: translateX(-100%) translateY(8px) scale(0.9);
|
||||
margin-left: 23px;
|
||||
}
|
||||
|
||||
&--right-oriented&--showed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
|
|
@ -25,7 +47,7 @@
|
|||
line-height: normal;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 5px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&--last {
|
||||
|
|
@ -52,7 +74,7 @@
|
|||
outline: none;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
margin: 6px 0 0;
|
||||
margin: 3px 0 0;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
@apply --overlay-pane;
|
||||
right: 5px;
|
||||
top: 35px;
|
||||
min-width: 124px;
|
||||
min-width: 114px;
|
||||
|
||||
@media (--mobile){
|
||||
bottom: 50px;
|
||||
|
|
@ -23,19 +23,19 @@
|
|||
|
||||
&--opened {
|
||||
display: block;
|
||||
animation-duration: 0.5s;
|
||||
animation-name: bounceIn;
|
||||
animation-duration: 0.1s;
|
||||
animation-name: panelShowing;
|
||||
}
|
||||
|
||||
&__plugin-zone {
|
||||
&:not(:empty){
|
||||
padding: 6px 6px 0;
|
||||
padding: 3px 3px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__default-zone {
|
||||
&:not(:empty){
|
||||
padding: 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,11 +43,11 @@
|
|||
@apply --toolbar-button;
|
||||
|
||||
&:not(:nth-child(3n+3)) {
|
||||
margin-right: 5px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
&:nth-child(n+4) {
|
||||
margin-top: 5px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
line-height: 32px;
|
||||
|
|
|
|||
|
|
@ -10,16 +10,9 @@
|
|||
display: none;
|
||||
|
||||
@media (--mobile) {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
height: 50px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 12px rgba(60, 67, 81, 0.18);
|
||||
transform: none !important;
|
||||
@apply --overlay-pane;
|
||||
padding: 3px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&--opened {
|
||||
|
|
@ -39,9 +32,7 @@
|
|||
display: flex;
|
||||
align-content: center;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
max-width: calc(100% - 70px);
|
||||
overflow-x: auto;
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,13 +41,16 @@
|
|||
|
||||
position: absolute;
|
||||
left: calc(var(--toolbox-buttons-size) * -1);
|
||||
flex-shrink: 0;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (--mobile){
|
||||
display: none !important;
|
||||
display: inline-flex !important;
|
||||
position: static;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +74,7 @@
|
|||
@media (--mobile){
|
||||
position: static;
|
||||
margin-left: auto;
|
||||
padding-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
position: static;
|
||||
transform: none !important;
|
||||
align-items: center;
|
||||
visibility: visible !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&--opened {
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
&__button {
|
||||
@apply --toolbox-button;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
|
|
@ -27,7 +28,6 @@
|
|||
top: 25px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 5px;
|
||||
line-height: 21px;
|
||||
opacity: 0;
|
||||
background: var(--bg-light);
|
||||
box-shadow: 0 10px 12px -9px rgba(26, 39, 54, 0.32), 0 3px 2px -2px rgba(33, 48, 73, 0.05);
|
||||
|
|
@ -41,6 +41,10 @@
|
|||
letter-spacing: 0.02em;
|
||||
line-height: 1em;
|
||||
|
||||
@media (--mobile) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-shortcut {
|
||||
color: rgba(100, 105, 122, 0.6);
|
||||
word-spacing: -2px;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@
|
|||
border-radius: 4px;
|
||||
z-index: 2;
|
||||
|
||||
@media (--mobile){
|
||||
box-shadow: 0 5px 9px -5px rgba(21, 40, 54, 0.49),6px 15px 34px -6px rgba(33, 48, 73, 0.54);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 15px;
|
||||
|
|
@ -72,6 +76,21 @@
|
|||
background-color: #fff;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&--left-oriented {
|
||||
&::before {
|
||||
left: 15px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--right-oriented {
|
||||
&::before {
|
||||
left: auto;
|
||||
right: 15px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -106,6 +125,7 @@
|
|||
width: 34px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
|
|
|||