Merge pull request #3188 from crazyserver/MOBILE-3814

Mobile 3814
main
Dani Palou 2022-03-18 09:45:28 +01:00 committed by GitHub
commit 6fe91f8aa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 219 additions and 124 deletions

View File

@ -26,7 +26,6 @@
min-height: var(--course-storage-max-activity-height); min-height: var(--course-storage-max-activity-height);
position: absolute; position: absolute;
@include position(0, 0, null, 0); @include position(0, 0, null, 0);
background: -webkit-linear-gradient(top, rgba(var(--background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--background-gradient-rgb), 1) calc(100% - 20px));
background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--background-gradient-rgb), 1) calc(100% - 20px)); background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - 30px), rgba(var(--background-gradient-rgb), 1) calc(100% - 20px));
z-index: 6; z-index: 6;
pointer-events: none; pointer-events: none;

View File

@ -90,8 +90,8 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
protected element: HTMLElement; protected element: HTMLElement;
protected emptyText = ''; protected emptyText = '';
protected contentSpan: HTMLElement; protected domPromises: CoreCancellablePromise<void>[] = [];
protected domPromise?: CoreCancellablePromise<void>; protected domElementPromise?: CoreCancellablePromise<void>;
constructor( constructor(
element: ElementRef, element: ElementRef,
@ -101,18 +101,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
CoreComponentsRegistry.register(element.nativeElement, this); CoreComponentsRegistry.register(element.nativeElement, this);
this.element = element.nativeElement; this.element = element.nativeElement;
this.element.classList.add('core-format-text-loading'); // Hide contents until they're treated. this.element.classList.add('core-loading'); // Hide contents until they're treated.
const placeholder = document.createElement('span');
placeholder.classList.add('core-format-text-loader');
this.element.appendChild(placeholder);
this.contentSpan = document.createElement('span');
this.contentSpan.classList.add('core-format-text-content');
this.element.appendChild(this.contentSpan);
this.emptyText = this.hideIfEmpty ? '' : '&nbsp;'; this.emptyText = this.hideIfEmpty ? '' : '&nbsp;';
this.contentSpan.innerHTML = this.emptyText; this.element.innerHTML = this.emptyText;
this.afterRender = new EventEmitter<void>(); this.afterRender = new EventEmitter<void>();
@ -134,14 +126,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.domPromise?.cancel(); this.domElementPromise?.cancel();
this.domPromises.forEach((promise) => { promise.cancel();});
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async ready(): Promise<void> { async ready(): Promise<void> {
if (!this.element.classList.contains('core-format-text-loading')) { if (!this.element.classList.contains('core-loading')) {
return; return;
} }
@ -229,7 +222,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
* Add magnifying glass icons to view adapted images at full size. * Add magnifying glass icons to view adapted images at full size.
*/ */
async addMagnifyingGlasses(): Promise<void> { async addMagnifyingGlasses(): Promise<void> {
const imgs = Array.from(this.contentSpan.querySelectorAll('.core-adapted-img-container > img')); const imgs = Array.from(this.element.querySelectorAll('.core-adapted-img-container > img'));
if (!imgs.length) { if (!imgs.length) {
return; return;
} }
@ -311,7 +304,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
*/ */
protected async finishRender(): Promise<void> { protected async finishRender(): Promise<void> {
// Show the element again. // Show the element again.
this.element.classList.remove('core-format-text-loading'); this.element.classList.remove('core-loading');
await CoreUtils.nextTick(); await CoreUtils.nextTick();
@ -324,7 +317,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
*/ */
protected async formatAndRenderContents(): Promise<void> { protected async formatAndRenderContents(): Promise<void> {
if (!this.text) { if (!this.text) {
this.contentSpan.innerHTML = this.emptyText; // Remove current contents. this.element.innerHTML = this.emptyText; // Remove current contents.
await this.finishRender(); await this.finishRender();
@ -342,10 +335,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
// Disable media adapt to correctly calculate the height. // Disable media adapt to correctly calculate the height.
this.element.classList.add('core-disable-media-adapt'); this.element.classList.add('core-disable-media-adapt');
this.contentSpan.innerHTML = ''; // Remove current contents. this.element.innerHTML = ''; // Remove current contents.
// Move the children to the current element to be able to calculate the height. // Move the children to the current element to be able to calculate the height.
CoreDomUtils.moveChildren(result.div, this.contentSpan); CoreDomUtils.moveChildren(result.div, this.element);
await CoreUtils.nextTick(); await CoreUtils.nextTick();
@ -362,7 +355,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
if (result.options.filter) { if (result.options.filter) {
// Let filters handle HTML. We do it here because we don't want them to block the render of the text. // Let filters handle HTML. We do it here because we don't want them to block the render of the text.
CoreFilterDelegate.handleHtml( CoreFilterDelegate.handleHtml(
this.contentSpan, this.element,
result.filters, result.filters,
this.viewContainerRef, this.viewContainerRef,
result.options, result.options,
@ -557,9 +550,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
* @return The width of the element in pixels. * @return The width of the element in pixels.
*/ */
protected async getElementWidth(): Promise<number> { protected async getElementWidth(): Promise<number> {
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); if (!this.domElementPromise) {
this.domElementPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domPromise; }
await this.domElementPromise;
let width = this.element.getBoundingClientRect().width; let width = this.element.getBoundingClientRect().width;
if (!width) { if (!width) {
@ -709,12 +703,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
newUrl += `&h=${privacyHash}`; newUrl += `&h=${privacyHash}`;
} }
const domPromise = CoreDomUtils.waitToBeInDOM(iframe);
this.domPromises.push(domPromise);
await domPromise;
// Width and height are mandatory, we need to calculate them. // Width and height are mandatory, we need to calculate them.
let width: string | number; let width: string | number;
let height: string | number; let height: string | number;
await CoreDomUtils.waitToBeInDOM(iframe, 5000);
if (iframe.width) { if (iframe.width) {
width = iframe.width; width = iframe.width;
} else { } else {

View File

@ -37,7 +37,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.visiblePromise = CoreDomUtils.waitToBeVisible(this.element); this.visiblePromise = CoreDomUtils.waitToBeInViewport(this.element);
await this.visiblePromise; await this.visiblePromise;

View File

@ -58,7 +58,7 @@ describe('CoreFormatTextDirective', () => {
); );
// Assert // Assert
const text = fixture.nativeElement.querySelector('core-format-text .core-format-text-content'); const text = fixture.nativeElement.querySelector('core-format-text');
expect(text).not.toBeNull(); expect(text).not.toBeNull();
expect(text.innerHTML).toEqual(sentence); expect(text.innerHTML).toEqual(sentence);
}); });

View File

@ -29,7 +29,9 @@
</ng-container> </ng-container>
<ion-label> <ion-label>
<h1>{{ title }}</h1> <h1>
<core-format-text [text]="title" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
</h1>
<div class="core-course-progress" *ngIf="progress !== undefined"> <div class="core-course-progress" *ngIf="progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
</core-progress-bar> </core-progress-bar>

View File

@ -110,7 +110,7 @@
</button> </button>
</ion-slide> </ion-slide>
<ion-slide *ngIf="isPhone"> <ion-slide *ngIf="isPhone">
<button [title]="'core.editor.hidetoolbar' | translate" (click)="hideToolbar($event)" (keyup)="hideToolbar($event)" <button [title]="'core.editor.hidetoolbar' | translate" (click)="hideToolbar($event, true)" (keyup)="hideToolbar($event, true)"
(mousedown)="downAction($event)" (keydown)="downAction($event)"> (mousedown)="downAction($event)" (keydown)="downAction($event)">
<ion-icon name="fas-times" aria-hidden="true"></ion-icon> <ion-icon name="fas-times" aria-hidden="true"></ion-icon>
</button> </button>

View File

@ -107,6 +107,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
protected languageChangedSubscription?: Subscription; protected languageChangedSubscription?: Subscription;
protected resizeListener?: CoreEventObserver; protected resizeListener?: CoreEventObserver;
protected domPromise?: CoreCancellablePromise<void>; protected domPromise?: CoreCancellablePromise<void>;
protected buttonsDomPromise?: CoreCancellablePromise<void>;
rteEnabled = false; rteEnabled = false;
isPhone = false; isPhone = false;
@ -176,8 +177,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
this.editorElement.oninput = this.onChange.bind(this); this.editorElement.oninput = this.onChange.bind(this);
this.editorElement.onkeydown = this.moveCursor.bind(this); this.editorElement.onkeydown = this.moveCursor.bind(this);
await CoreDomUtils.waitToBeVisible(this.editorElement);
// Use paragraph on enter. // Use paragraph on enter.
document.execCommand('DefaultParagraphSeparator', false, 'p'); document.execCommand('DefaultParagraphSeparator', false, 'p');
@ -339,7 +338,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
return; return;
} }
if (this.isNullOrWhiteSpace(this.editorElement.innerText)) { if (this.isNullOrWhiteSpace(this.editorElement.textContent)) {
this.clearText(); this.clearText();
} else { } else {
// The textarea and the form control must receive the original URLs. // The textarea and the form control must receive the original URLs.
@ -718,8 +717,17 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
/** /**
* Hide the toolbar in phone mode. * Hide the toolbar in phone mode.
*
* @param event Event.
* @param force If true it will not check the target of the event.
*/ */
hideToolbar(event: Event): void { hideToolbar(event: Event, force = false): void {
if (!force && event.target && this.element.contains(event.target as HTMLElement)) {
// Do not hide if clicked inside the editor area, except forced.
return;
}
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return; return;
} }
@ -836,7 +844,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
const length = await this.toolbarSlides.length(); const length = await this.toolbarSlides.length();
await CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement, 5000); // Cancel previous one, if any.
this.buttonsDomPromise?.cancel();
this.buttonsDomPromise = CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement);
await this.buttonsDomPromise;
const width = this.toolbar.nativeElement.getBoundingClientRect().width; const width = this.toolbar.nativeElement.getBoundingClientRect().width;
@ -1097,6 +1108,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
this.labelObserver?.disconnect(); this.labelObserver?.disconnect();
this.resizeListener?.off(); this.resizeListener?.off();
this.domPromise?.cancel(); this.domPromise?.cancel();
this.buttonsDomPromise?.cancel();
} }
} }

View File

@ -97,10 +97,9 @@ export class CoreDomUtilsProvider {
* Wait an element to be in dom of another element. * Wait an element to be in dom of another element.
* *
* @param element Element to wait. * @param element Element to wait.
* @param timeout If defined, timeout to wait before rejecting the promise.
* @return Cancellable promise. * @return Cancellable promise.
*/ */
waitToBeInDOM(element: HTMLElement, timeout?: number): CoreCancellablePromise<void> { waitToBeInDOM(element: HTMLElement): CoreCancellablePromise<void> {
const root = element.getRootNode({ composed: true }); const root = element.getRootNode({ composed: true });
if (root === document) { if (root === document) {
@ -109,33 +108,24 @@ export class CoreDomUtilsProvider {
} }
let observer: MutationObserver; let observer: MutationObserver;
let observerTimeout: number | undefined;
return new CoreCancellablePromise<void>( return new CoreCancellablePromise<void>(
(resolve, reject) => { (resolve) => {
observer = new MutationObserver(() => { observer = new MutationObserver(() => {
const root = element.getRootNode({ composed: true }); const root = element.getRootNode({ composed: true });
if (root === document) { if (root !== document) {
return;
}
observer?.disconnect(); observer?.disconnect();
observerTimeout && clearTimeout(observerTimeout);
resolve(); resolve();
}
}); });
if (timeout) {
observerTimeout = window.setTimeout(() => {
observer?.disconnect();
reject(new Error('Waiting for DOM timeout reached'));
}, timeout);
}
observer.observe(document.body, { subtree: true, childList: true }); observer.observe(document.body, { subtree: true, childList: true });
}, },
() => { () => {
observer?.disconnect(); observer?.disconnect();
observerTimeout && clearTimeout(observerTimeout);
}, },
); );
} }
@ -151,6 +141,7 @@ export class CoreDomUtilsProvider {
let interval: number | undefined; let interval: number | undefined;
// Mutations did not observe for visibility properties.
return new CoreCancellablePromise<void>( return new CoreCancellablePromise<void>(
async (resolve) => { async (resolve) => {
await domPromise; await domPromise;
@ -175,6 +166,60 @@ export class CoreDomUtilsProvider {
); );
} }
/**
* Wait an element to be in dom and visible.
*
* @param element Element to wait.
* @param intersectionRatio Intersection ratio (From 0 to 1).
* @return Cancellable promise.
*/
waitToBeInViewport(element: HTMLElement, intersectionRatio = 1): CoreCancellablePromise<void> {
const visiblePromise = CoreDomUtils.waitToBeVisible(element);
let intersectionObserver: IntersectionObserver;
let interval: number | undefined;
return new CoreCancellablePromise<void>(
async (resolve) => {
await visiblePromise;
if (CoreDomUtils.isElementInViewport(element, intersectionRatio)) {
return resolve();
}
if ('IntersectionObserver' in window) {
intersectionObserver = new IntersectionObserver((observerEntries) => {
const isIntersecting = observerEntries
.some((entry) => entry.isIntersecting && entry.intersectionRatio >= intersectionRatio);
if (!isIntersecting) {
return;
}
resolve();
intersectionObserver?.disconnect();
});
intersectionObserver.observe(element);
} else {
interval = window.setInterval(() => {
if (!CoreDomUtils.isElementInViewport(element, intersectionRatio)) {
return;
}
resolve();
window.clearInterval(interval);
}, 50);
}
},
() => {
visiblePromise.cancel();
intersectionObserver?.disconnect();
window.clearInterval(interval);
},
);
}
/** /**
* Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much. * Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much.
* This function helps setting up the debounce feature and remove listener easily. * This function helps setting up the debounce feature and remove listener easily.
@ -880,10 +925,21 @@ export class CoreDomUtilsProvider {
return elementPoint > window.innerHeight || elementPoint < scrollTopPos; return elementPoint > window.innerHeight || elementPoint < scrollTopPos;
} }
/**
* Check whether an element has been added to the DOM.
*
* @param element Element.
* @return True if element has been added to the DOM, false otherwise.
*/
isElementInDom(element: HTMLElement): boolean {
return element.getRootNode({ composed: true }) === document;
}
/** /**
* Check whether an element is visible or not. * Check whether an element is visible or not.
* *
* @param element Element. * @param element Element.
* @return True if element is visible inside the DOM.
*/ */
isElementVisible(element: HTMLElement): boolean { isElementVisible(element: HTMLElement): boolean {
if (element.clientWidth === 0 || element.clientHeight === 0) { if (element.clientWidth === 0 || element.clientHeight === 0) {
@ -895,7 +951,35 @@ export class CoreDomUtilsProvider {
return false; return false;
} }
return element.offsetParent !== null; return CoreDomUtils.isElementInDom(element);
}
/**
* Check whether an element is intersecting the intersectionRatio in viewport.
*
* @param element
* @param intersectionRatio Intersection ratio (From 0 to 1).
* @return True if in viewport.
*/
isElementInViewport(element: HTMLElement, intersectionRatio = 1): boolean {
const elementRectangle = element.getBoundingClientRect();
const elementArea = elementRectangle.width * elementRectangle.height;
if (elementArea == 0) {
return false;
}
const intersectionRectangle = {
top: Math.max(0, elementRectangle.top),
left: Math.max(0, elementRectangle.left),
bottom: Math.min(window.innerHeight, elementRectangle.bottom),
right: Math.min(window.innerWidth, elementRectangle.right),
};
const intersectionArea = (intersectionRectangle.right - intersectionRectangle.left) *
(intersectionRectangle.bottom - intersectionRectangle.top);
return intersectionArea / elementArea >= intersectionRatio;
} }
/** /**

View File

@ -51,7 +51,7 @@ export class CoreIframeUtilsProvider {
protected waitAutoLoginDefer?: PromiseDefer<void>; protected waitAutoLoginDefer?: PromiseDefer<void>;
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreUtilsProvider'); this.logger = CoreLogger.getInstance('CoreIframeUtilsProvider');
} }
/** /**
@ -237,7 +237,7 @@ export class CoreIframeUtilsProvider {
contentDocument = 'contentDocument' in element && element.contentDocument contentDocument = 'contentDocument' in element && element.contentDocument
? element.contentDocument ? element.contentDocument
: contentWindow && contentWindow.document; : contentWindow && contentWindow.document;
} catch (ex) { } catch {
// Ignore errors. // Ignore errors.
} }
@ -250,7 +250,7 @@ export class CoreIframeUtilsProvider {
// It's probably an <embed>. Try to get the window and the document. // It's probably an <embed>. Try to get the window and the document.
try { try {
contentDocument = element.getSVGDocument(); contentDocument = element.getSVGDocument();
} catch (ex) { } catch {
// Ignore errors. // Ignore errors.
} }
@ -310,10 +310,10 @@ export class CoreIframeUtilsProvider {
}; };
} }
if (contentDocument) { if (contentDocument.body) {
// Search sub frames. // Search sub frames.
CoreIframeUtilsProvider.FRAME_TAGS.forEach((tag) => { CoreIframeUtilsProvider.FRAME_TAGS.forEach((tag) => {
const elements = Array.from(contentDocument.querySelectorAll(tag)); const elements = Array.from(contentDocument.body.querySelectorAll(tag));
elements.forEach((subElement: CoreFrameElement) => { elements.forEach((subElement: CoreFrameElement) => {
this.treatFrame(subElement, true); this.treatFrame(subElement, true);
}); });
@ -333,6 +333,8 @@ export class CoreIframeUtilsProvider {
return; return;
} }
element.classList.add('core-loading');
const treatElement = (sendResizeEvent: boolean = false) => { const treatElement = (sendResizeEvent: boolean = false) => {
this.checkOnlineFrameInOffline(element, isSubframe); this.checkOnlineFrameInOffline(element, isSubframe);
@ -348,9 +350,12 @@ export class CoreIframeUtilsProvider {
this.treatFrameLinks(element, document); this.treatFrameLinks(element, document);
} }
// Iframe content has been loaded.
// Send a resize events to the iframe so it calculates the right size if needed. // Send a resize events to the iframe so it calculates the right size if needed.
if (window && sendResizeEvent) { if (window && sendResizeEvent) {
setTimeout(() => window.dispatchEvent(new Event('resize')), 1000); element.classList.remove('core-loading');
setTimeout(() => window.dispatchEvent && window.dispatchEvent(new Event('resize')), 1000);
} }
}; };

View File

@ -75,7 +75,6 @@
height: 60px; height: 60px;
position: absolute; position: absolute;
@include position(null, 0, 0, 0); @include position(null, 0, 0, 0);
background: -webkit-linear-gradient(top, rgba(var(--background-gradient-rgb), 0) calc(100% - var(--gradient-size)), rgba(var(--background-gradient-rgb), 1) calc(100% - 4px));
background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - var(--gradient-size)), rgba(var(--background-gradient-rgb), 1) calc(100% - 4px)); background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - var(--gradient-size)), rgba(var(--background-gradient-rgb), 1) calc(100% - 4px));
z-index: 6; z-index: 6;
} }

View File

@ -5,59 +5,25 @@
core-format-text { core-format-text {
--core-format-text-background: var(--background, var(--ion-item-background)); --core-format-text-background: var(--background, var(--ion-item-background));
--core-format-text-viewer-icon-background: rgba(255, 255, 255, .5); --core-format-text-viewer-icon-background: rgba(255, 255, 255, .5);
--core-format-text-loader-shine: 251,251,251;
} }
body.dark core-format-text { body.dark core-format-text {
--core-format-text-viewer-icon-background: rgba(0, 0, 0, .5); --core-format-text-viewer-icon-background: rgba(0, 0, 0, .5);
--core-format-text-loader-shine: 90,90,90;
} }
core-format-text { core-format-text {
display: contents;
.core-format-text-loader {
opacity: 0;
@include core-transition(opacity, 200ms);
display: contents;
}
&.core-format-text-loading {
position: relative;
width: 100%;
height: 100%;
opacity: 1;
background-color: rgba(0,0,0,.1);
overflow: hidden;
display: block;
border-radius: var(--small-radius);
.core-format-text-loader {
position: absolute;
left: -45%;
height: 100%;
width: 45%;
background-image: -webkit-linear-gradient(to left, rgba(var(--core-format-text-loader-shine), .05), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .6), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .05));
background-image: linear-gradient(to left, rgba(var(--core-format-text-loader-shine), .05), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .6), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .05));
animation: loading 1s infinite;
opacity: 1;
display: inline;
}
.core-format-text-content {
opacity: 0;
display: inline;
}
}
.core-format-text-content {
opacity: 1;
@include core-transition(opacity, 200ms);
display: contents; display: contents;
user-select: text; user-select: text;
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
@include core-transition(background-color color, 200ms);
&.core-loading {
width: 100%;
&:empty:before {
content: 'E'; // Set a minimum empty text to have a minimum height of one line.
opacity: 0;
}
} }
&.collapsible-item { &.collapsible-item {
@ -66,16 +32,11 @@ core-format-text {
cursor: pointer; cursor: pointer;
pointer-events: auto; pointer-events: auto;
.core-format-text-content {
display: block;
}
&.collapsible-enabled { &.collapsible-enabled {
.core-format-text-content {
display: block; display: block;
max-height: none; max-height: none;
}
&.collapsible-collapsed .core-format-text-content { &.collapsible-collapsed {
overflow: hidden; overflow: hidden;
} }
} }
@ -97,7 +58,7 @@ core-format-text {
} }
&.collapsible-item.inline { &.collapsible-item.inline {
display: inline-block; display: inline-block;
&.collapsible-enabled .core-format-text-content { &.collapsible-enabled {
display: inline-block; display: inline-block;
} }
} }
@ -141,7 +102,7 @@ core-format-text {
} }
// Disable clicks in links inside MathJax equations. // Disable clicks in links inside MathJax equations.
.core-format-text-content .filter_mathjaxloader_equation .MathJax_Preview a { .filter_mathjaxloader_equation .MathJax_Preview a {
pointer-events: none; pointer-events: none;
} }
@ -243,7 +204,7 @@ core-format-text {
} }
core-format-text .core-format-text-content, core-format-text,
core-rich-text-editor .core-rte-editor { core-rich-text-editor .core-rte-editor {
@include core-headings(); @include core-headings();
@ -657,14 +618,10 @@ core-rich-text-editor .core-rte-editor {
// h1 is too big and ugly, reduce size when loading. // h1 is too big and ugly, reduce size when loading.
ion-header.ios h1 core-format-text { ion-header.ios h1 core-format-text {
&.core-format-text-loading { &.core-loading {
max-height: 30px; max-height: 30px;
margin-top: 10px; margin-top: 10px;
} }
&.core-format-text-content {
display: block;
margin-top: -10px;
}
} }
body.dark core-format-text select, body.dark core-format-text select,

View File

@ -120,15 +120,15 @@ body {
} }
// Correctly inherit ion-text-wrap onto labels. // Correctly inherit ion-text-wrap onto labels.
.item ion-label core-format-text .core-format-text-content > *, .item ion-label core-format-text > *,
.fake-ion-item core-format-text .core-format-text-content > * { .fake-ion-item core-format-text > * {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.item.ion-text-wrap > ion-label core-format-text .core-format-text-content > *, .item.ion-text-wrap > ion-label core-format-text > *,
.fake-ion-item.ion-text-wrap core-format-text .core-format-text-content > * { .fake-ion-item.ion-text-wrap core-format-text > * {
white-space: normal; white-space: normal;
overflow: inherit; overflow: inherit;
} }
@ -334,7 +334,7 @@ ion-button.button-solid {
--box-shadow: none; --box-shadow: none;
} }
ion-button core-format-text .core-format-text-content { ion-button core-format-text {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -350,7 +350,7 @@ ion-button > * {
ion-button.ion-text-wrap { ion-button.ion-text-wrap {
white-space: normal; white-space: normal;
core-format-text .core-format-text-content { core-format-text {
white-space: normal; white-space: normal;
display: contents; display: contents;
} }
@ -1516,3 +1516,36 @@ ion-header.no-title {
flex-grow: 1; flex-grow: 1;
} }
} }
// Loader animation.
.core-loading {
position: relative;
background-color: var(--loader-background-color);
color: transparent; // Hide contents.
overflow: hidden;
display: var(--loader-display);
border-radius: var(--loader-radius);
@include core-transition(all, 200ms);
min-height: 8px;
min-width: 50px;
// Hide contents.
> * {
opacity: 0;
@include core-transition(opacity, 200ms);
}
&::after {
content: '';
position: absolute;
left: -45%;
height: 100%;
width: 45%;
background-image: linear-gradient(to left, rgba(var(--loader-shine), .05), rgba(var(--loader-shine), .3), rgba(var(--loader-shine), .6), rgba(var(--loader-shine), .3), rgba(var(--loader-shine), .05));
animation: loading 1s infinite;
display: block;
top: 0;
bottom: 0;
}
}

View File

@ -41,6 +41,8 @@
--contrast-background: var(--gray-900); --contrast-background: var(--gray-900);
--loader-shine: 90, 90, 90;
--drop-shadow-color: 0, 0, 0, 1; --drop-shadow-color: 0, 0, 0, 1;
--drop-shadow-top: 0px 2px 5px rgba(var(--drop-shadow-color)); --drop-shadow-top: 0px 2px 5px rgba(var(--drop-shadow-color));
--drop-shadow-bottom: 0px -2px 5px rgba(var(--drop-shadow-color)); --drop-shadow-bottom: 0px -2px 5px rgba(var(--drop-shadow-color));

View File

@ -85,6 +85,11 @@
--contrast-background: white; --contrast-background: white;
--loader-background-color: rgba(0, 0, 0, .1);
--loader-shine: 251, 251, 251;
--loader-radius: var(--small-radius);
--loader-display: block;
--drop-shadow-color: 0, 0, 0, 0.5; --drop-shadow-color: 0, 0, 0, 0.5;
--drop-shadow-top: 0px 2px 5px rgba(var(--drop-shadow-color)); --drop-shadow-top: 0px 2px 5px rgba(var(--drop-shadow-color));
--drop-shadow-bottom: 0px -2px 5px rgba(var(--drop-shadow-color)); --drop-shadow-bottom: 0px -2px 5px rgba(var(--drop-shadow-color));