commit
e016875439
|
@ -1,11 +1,11 @@
|
|||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item button class="ion-text-wrap" (click)="onItemClick(item)" *ngFor="let item of items" [detail]="false"
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="onItemClick(item)" *ngFor="let item of items" [detail]="false"
|
||||
[attr.aria-label]="item.text | translate">
|
||||
<ion-icon [name]="item.icon" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ item.text | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-icon [name]="item.icon" slot="end" aria-hidden="true" />
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
<ion-radio-group *ngIf="item.templateName === 'multichoice-r'" [(ngModel)]="item.value" [required]="item.required"
|
||||
name="{{item.typ}}_{{item.id}}">
|
||||
<ion-item *ngFor="let option of item.choices" class="ion-text-wrap">
|
||||
<ion-item *ngFor="let option of item.choices" class="ion-text-wrap" lines="none">
|
||||
<ion-radio [value]="option.value">
|
||||
<core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
|
||||
contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true" [courseId]="courseId" />
|
||||
|
@ -80,7 +80,7 @@
|
|||
<ng-container *ngIf="item.templateName === 'multichoice-c'">
|
||||
<ion-item *ngFor="let option of item.choices">
|
||||
<ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="option.checked"
|
||||
value="option.value">
|
||||
value="option.value" lines="none">
|
||||
<core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
|
||||
contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true" [courseId]="courseId" />
|
||||
</ion-checkbox>
|
||||
|
@ -156,7 +156,7 @@
|
|||
|
||||
|
||||
<ng-template #label let-item="item">
|
||||
<p *ngIf="item.name" [core-mark-required]="item.required" [slot]="item.slottedLabel ? 'label' : undefined">
|
||||
<p class="ion-text-wrap" *ngIf="item.name" [core-mark-required]="item.required" [slot]="item.slottedLabel ? 'label' : undefined">
|
||||
<span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span>
|
||||
<core-format-text [component]="component" [componentId]="cmId" [text]="item.name" contextLevel="module" [contextInstanceId]="cmId"
|
||||
[courseId]="courseId" [wsNotFiltered]="true" />
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 27 KiB |
|
@ -55,7 +55,7 @@
|
|||
<!-- Template for units entered using radio buttons. -->
|
||||
<ng-template #radioUnits>
|
||||
<ion-radio-group [(ngModel)]="question!.unit" [name]="question!.optionsName">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let option of question!.options">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let option of question!.options" lines="none">
|
||||
<ion-radio [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
|
||||
[color]="question!.input?.correctIconColor">
|
||||
{{ option.text }}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<!-- Radio buttons for single choice. -->
|
||||
<ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
|
||||
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
|
||||
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options" lines="none">
|
||||
<ion-radio [value]="option.value" [disabled]="option.disabled"
|
||||
[color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")'>
|
||||
<div>
|
||||
|
|
|
@ -133,6 +133,7 @@ export class PageLoadsManager {
|
|||
component: CoreRefreshButtonModalComponent,
|
||||
cssClass: 'core-modal-no-background core-modal-fullscreen',
|
||||
closeOnNavigate: true,
|
||||
showBackdrop: false,
|
||||
});
|
||||
|
||||
this.onRefreshPage.next();
|
||||
|
|
|
@ -1,38 +1,41 @@
|
|||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('download')" *ngIf="downloadCourseEnabled" [detail]="false">
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="action('download')" *ngIf="downloadCourseEnabled"
|
||||
[detail]="false">
|
||||
<ion-icon *ngIf="!prefetch.loading" [name]="prefetch.icon" slot="start" aria-hidden="true" />
|
||||
<ion-spinner *ngIf="prefetch.loading" slot="start" [attr.aria-label]="'core.loading' | translate" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ prefetch.statusTranslatable | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('delete')" [detail]="false"
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="action('delete')" [detail]="false"
|
||||
*ngIf="prefetch.status === 'downloaded' || prefetch.status === 'outdated'">
|
||||
<ion-icon name="fas-trash" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'addon.storagemanager.deletedata' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('hide')" *ngIf="!course.hidden" [detail]="false">
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="action('hide')" *ngIf="!course.hidden" [detail]="false">
|
||||
<ion-icon name="fas-eye" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.courses.hidecourse' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('show')" *ngIf="course.hidden" [detail]="false">
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="action('show')" *ngIf="course.hidden" [detail]="false">
|
||||
<ion-icon name="fas-eye-slash" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.courses.show' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('favourite')" *ngIf="!course.isfavourite" [detail]="false">
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="action('favourite')" *ngIf="!course.isfavourite"
|
||||
[detail]="false">
|
||||
<ion-icon name="fas-star" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.courses.addtofavourites' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('unfavourite')" *ngIf="course.isfavourite" [detail]="false">
|
||||
<ion-item button class="ion-text-wrap icon-margin-reduced" (click)="action('unfavourite')" *ngIf="course.isfavourite"
|
||||
[detail]="false">
|
||||
<ion-icon name="far-star" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.courses.removefromfavourites' | translate }}</p>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
|
||||
<div class="core-rte-editor-container" (click)="focusRTE($event)" [class.toolbar-hidden]="toolbarHidden">
|
||||
<div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true" [class.empty]="isEmpty"
|
||||
[attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)"
|
||||
(blur)="hideToolbar($event)" (keydown)="onKeyDown($event)">
|
||||
[attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="focusRTE($event)"
|
||||
(blur)="blurRTE($event)" (keydown)="onKeyDown($event)">
|
||||
</div>
|
||||
|
||||
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
|
||||
[placeholder]="placeholder" [attr.aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="showToolbar($event)"
|
||||
(ionBlur)="hideToolbar($event)" />
|
||||
[placeholder]="placeholder" [attr.aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="focusRTE($event)"
|
||||
(ionBlur)="blurRTE($event)" />
|
||||
|
||||
<div class="core-rte-info-message" *ngIf="infoMessage">
|
||||
<ion-icon name="fas-circle-info" aria-hidden="true" />
|
||||
|
@ -23,16 +23,16 @@
|
|||
<swiper-container #swiperRef [slidesPerView]="swiperOpts.slidesPerView" (slidechangetransitionend)="updateToolbarArrows()">
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
|
||||
<swiper-slide>
|
||||
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strong" [title]="'core.editor.bold' | translate"
|
||||
(click)="buttonAction($event, 'bold', 'strong')" (keyup)="buttonAction($event, 'bold', 'strong')"
|
||||
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
|
||||
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strong || toolbarStyles.b"
|
||||
[title]="'core.editor.bold' | translate" (click)="buttonAction($event, 'bold', 'b')"
|
||||
(keyup)="buttonAction($event, 'bold', 'b')" (mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
|
||||
<ion-icon name="fas-bold" aria-hidden="true" />
|
||||
</button>
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.em" [title]="'core.editor.italic' | translate"
|
||||
(click)="buttonAction($event, 'italic', 'em')" (keyup)="buttonAction($event, 'italic', 'em')"
|
||||
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
|
||||
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.em || toolbarStyles.i"
|
||||
[title]="'core.editor.italic' | translate" (click)="buttonAction($event, 'italic', 'i')"
|
||||
(keyup)="buttonAction($event, 'italic', 'i')" (mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
|
||||
<ion-icon name="fas-italic" aria-hidden="true" />
|
||||
</button>
|
||||
</swiper-slide>
|
||||
|
@ -94,13 +94,13 @@
|
|||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'removeFormat')" (keyup)="buttonAction($event, 'removeFormat')"
|
||||
(mousedown)="downAction($event)" (keydown)="downAction($event)" [title]="'core.editor.clear' | translate">
|
||||
(mousedown)="downAction($event)" (keydown)="downAction($event)" [title]="'core.editor.clear' | translate" tabindex="0">
|
||||
<ion-icon name="fas-eraser" aria-hidden="true" />
|
||||
</button>
|
||||
</swiper-slide>
|
||||
<swiper-slide *ngIf="canScanQR">
|
||||
<button [disabled]="!rteEnabled" (click)="scanQR($event)" (keyup)="scanQR($event)" (mousedown)="stopBubble($event)"
|
||||
(keydown)="stopBubble($event)" [title]="'core.scanqr' | translate">
|
||||
(keydown)="stopBubble($event)" [title]="'core.scanqr' | translate" tabindex="0">
|
||||
<ion-icon name="fas-qrcode" aria-hidden="true" />
|
||||
</button>
|
||||
</swiper-slide>
|
||||
|
@ -110,12 +110,6 @@
|
|||
<ion-icon name="fas-code" aria-hidden="true" />
|
||||
</button>
|
||||
</swiper-slide>
|
||||
<swiper-slide *ngIf="isPhone">
|
||||
<button [title]="'core.editor.hidetoolbar' | translate" (click)="hideToolbar($event, true)" (keyup)="hideToolbar($event, true)"
|
||||
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
|
||||
<ion-icon name="fas-xmark" aria-hidden="true" />
|
||||
</button>
|
||||
</swiper-slide>
|
||||
</swiper-container>
|
||||
<button *ngIf="toolbarArrows" class="toolbar-arrow" [attr.disabled]="toolbarNextHidden ? 'true' : null"
|
||||
[attr.aria-label]="'core.next' | translate" (click)="toolbarNext($event)" (keyup)="toolbarNext($event)"
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
border: 1px solid var(--stroke);
|
||||
border-radius: var(--mdl-shape-borderRadius-md);
|
||||
|
||||
&:focus-within {
|
||||
&.has-focus {
|
||||
border-color: var(--a11y-focus-color);
|
||||
border-width: 2px;
|
||||
outline: none !important;
|
||||
|
@ -98,9 +98,18 @@
|
|||
.core-textarea {
|
||||
position: relative;
|
||||
|
||||
--highlight-color: transparent !important;
|
||||
--full-highlight-height: 0px !important;
|
||||
|
||||
::ng-deep .textarea-wrapper,
|
||||
::ng-deep .textarea-legacy-wrapper {
|
||||
height: 100%;
|
||||
|
||||
.textarea-wrapper-inner {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::ng-deep textarea {
|
||||
caret-color: black;
|
||||
margin: 0px !important;
|
||||
padding: 0px;
|
||||
resize: none;
|
||||
|
@ -112,6 +121,7 @@
|
|||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.core-rte-toolbar {
|
||||
display: flex;
|
||||
|
|
|
@ -78,9 +78,13 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
@Input() draftExtraParams?: Record<string, unknown>; // Extra params to identify the draft.
|
||||
@Output() contentChanged: EventEmitter<string | undefined | null>;
|
||||
|
||||
@ViewChild('editor') editor?: ElementRef; // WYSIWYG editor.
|
||||
protected editorElement?: HTMLDivElement; // WYSIWYG editor.
|
||||
@ViewChild('editor') editor?: ElementRef<HTMLDivElement>;
|
||||
|
||||
@ViewChild('toolbar') toolbar?: ElementRef<HTMLDivElement>;
|
||||
|
||||
protected textareaElement?: HTMLTextAreaElement;
|
||||
@ViewChild('textarea') textarea?: IonTextarea; // Textarea editor.
|
||||
@ViewChild('toolbar') toolbar?: ElementRef;
|
||||
|
||||
protected toolbarSlides?: Swiper;
|
||||
@ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) {
|
||||
|
@ -88,7 +92,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
* This setTimeout waits for Ionic's async initialization to complete.
|
||||
* Otherwise, an outdated swiper reference will be used.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
await this.waitLoadingsDone();
|
||||
|
||||
const swiper = CoreSwiper.initSwiperIfAvailable(this.toolbarSlides, swiperRef, this.swiperOpts);
|
||||
if (!swiper) {
|
||||
return;
|
||||
|
@ -102,8 +108,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
protected readonly RESTORE_MESSAGE_CLEAR_TIME = 6000;
|
||||
protected readonly SAVE_MESSAGE_CLEAR_TIME = 2000;
|
||||
|
||||
protected element: HTMLDivElement;
|
||||
protected editorElement?: HTMLDivElement;
|
||||
protected element: HTMLElement;
|
||||
protected minHeight = 200; // Minimum height of the editor.
|
||||
|
||||
protected valueChangeSubscription?: Subscription;
|
||||
|
@ -127,6 +132,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
protected domPromise?: CoreCancellablePromise<void>;
|
||||
protected buttonsDomPromise?: CoreCancellablePromise<void>;
|
||||
protected shortcutCommands?: Record<string, EditorCommand>;
|
||||
protected blurTimeout?: number;
|
||||
|
||||
rteEnabled = false;
|
||||
isPhone = false;
|
||||
|
@ -139,7 +145,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
infoMessage?: string;
|
||||
toolbarStyles = {
|
||||
strong: 'false',
|
||||
b: 'false',
|
||||
em: 'false',
|
||||
i: 'false',
|
||||
u: 'false',
|
||||
strike: 'false',
|
||||
p: 'false',
|
||||
|
@ -163,7 +171,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
elementRef: ElementRef,
|
||||
) {
|
||||
this.contentChanged = new EventEmitter<string>();
|
||||
this.element = elementRef.nativeElement as HTMLDivElement;
|
||||
this.element = elementRef.nativeElement;
|
||||
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
|
||||
}
|
||||
|
||||
|
@ -186,6 +194,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
|
||||
// Setup the editor.
|
||||
this.editorElement = this.editor?.nativeElement as HTMLDivElement;
|
||||
this.textareaElement = await this.textarea?.getInputElement();
|
||||
this.setContent(this.control?.value);
|
||||
this.originalContent = this.control?.value ?? undefined;
|
||||
this.lastDraft = this.control?.value ?? '';
|
||||
|
@ -216,11 +225,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
this.deleteDraftOnSubmitOrCancel();
|
||||
}
|
||||
|
||||
const ionItem = this.element.closest<HTMLIonItemElement>('ion-item');
|
||||
if (!ionItem) {
|
||||
return;
|
||||
}
|
||||
ionItem.classList.add('item-rte');
|
||||
this.setupIonItem();
|
||||
|
||||
if (this.editorElement) {
|
||||
const debounceMutation = CoreUtils.debounce(() => {
|
||||
|
@ -230,9 +235,19 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
this.contentObserver = new MutationObserver(debounceMutation);
|
||||
this.contentObserver.observe(this.editorElement, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Ion Item adding classes and managing aria-labelledby.
|
||||
*/
|
||||
protected setupIonItem(): void {
|
||||
const ionItem = this.element.closest<HTMLIonItemElement>('ion-item');
|
||||
if (!ionItem) {
|
||||
return;
|
||||
}
|
||||
ionItem.classList.add('item-rte');
|
||||
|
||||
const label = ionItem.querySelector('ion-label');
|
||||
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
|
@ -418,65 +433,21 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
* Set the caret position on the character number.
|
||||
*
|
||||
* @param parent Parent where to set the position.
|
||||
* @param chars Number of chars where to place the caret. If not defined it will go to the end.
|
||||
*/
|
||||
protected setCurrentCursorPosition(parent: Node, chars?: number): void {
|
||||
/**
|
||||
* Loops round all the child text nodes within the supplied node and sets a range from the start of the initial node to
|
||||
* the characters.
|
||||
*
|
||||
* @param node Node where to start.
|
||||
* @param range Previous calculated range.
|
||||
* @param chars Object with counting of characters (input-output param).
|
||||
* @param chars.count Count of characters.
|
||||
* @returns Selection range.
|
||||
*/
|
||||
const setRange = (node: Node, range: Range, chars: { count: number }): Range => {
|
||||
if (chars.count === 0) {
|
||||
range.setEnd(node, 0);
|
||||
} else if (node && chars.count > 0) {
|
||||
if (node.hasChildNodes()) {
|
||||
// Navigate through children.
|
||||
for (let lp = 0; lp < node.childNodes.length; lp++) {
|
||||
range = setRange(node.childNodes[lp], range, chars);
|
||||
protected setCurrentCursorPosition(parent: Node): void {
|
||||
const range = document.createRange();
|
||||
|
||||
if (chars.count === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ((node.textContent || '').length < chars.count) {
|
||||
// Jump this node.
|
||||
// @todo empty nodes will be omitted.
|
||||
chars.count -= (node.textContent || '').length;
|
||||
} else {
|
||||
// The cursor will be placed in this element.
|
||||
range.setEnd(node, chars.count);
|
||||
chars.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
let range = document.createRange();
|
||||
if (chars === undefined) {
|
||||
// Select all so it will go to the end.
|
||||
range.selectNode(parent);
|
||||
range.selectNodeContents(parent);
|
||||
} else if (chars < 0 || chars > (parent.textContent || '').length) {
|
||||
return;
|
||||
} else {
|
||||
range.selectNode(parent);
|
||||
range.setStart(parent, 0);
|
||||
range = setRange(parent, range, { count: chars });
|
||||
}
|
||||
|
||||
if (range) {
|
||||
const selection = window.getSelection();
|
||||
range.collapse(false);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -491,26 +462,27 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
|
||||
this.stopBubble(event);
|
||||
|
||||
// Update tags for a11y.
|
||||
this.replaceTags(['b', 'i'], ['strong', 'em']);
|
||||
|
||||
this.setContent(this.control?.value || '');
|
||||
|
||||
this.rteEnabled = !this.rteEnabled;
|
||||
|
||||
// Set focus and cursor at the end.
|
||||
// Modify the DOM directly so the keyboard stays open.
|
||||
if (this.rteEnabled) {
|
||||
// Update tags for a11y.
|
||||
this.replaceTags(['b', 'i'], ['strong', 'em']);
|
||||
|
||||
if (this.rteEnabled) {
|
||||
this.editorElement?.removeAttribute('hidden');
|
||||
const textareaInputElement = await this.textarea?.getInputElement();
|
||||
textareaInputElement?.setAttribute('hidden', '');
|
||||
this.editorElement?.focus();
|
||||
this.textareaElement?.setAttribute('hidden', '');
|
||||
} else {
|
||||
this.editorElement?.setAttribute('hidden', '');
|
||||
const textareaInputElement = await this.textarea?.getInputElement();
|
||||
textareaInputElement?.removeAttribute('hidden');
|
||||
this.textarea?.setFocus();
|
||||
this.textareaElement?.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
await CoreUtils.nextTick();
|
||||
|
||||
this.focusRTE(event);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -679,13 +651,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
document.execCommand(command, false);
|
||||
|
||||
// Modern browsers are using non a11y tags, so replace them.
|
||||
if (command === 'bold') {
|
||||
this.replaceTags(['b'], ['strong']);
|
||||
} else if (command === 'italic') {
|
||||
this.replaceTags(['i'], ['em']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -706,33 +671,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
}
|
||||
|
||||
/**
|
||||
* Focus editor when click the area.
|
||||
*/
|
||||
focusRTE(): void {
|
||||
if (this.rteEnabled) {
|
||||
this.editorElement?.focus();
|
||||
} else {
|
||||
this.textarea?.setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the toolbar in phone mode.
|
||||
* Blur and hide the toolbar in phone mode.
|
||||
*
|
||||
* @param event Event.
|
||||
* @param force If true it will not check the target of the event.
|
||||
*/
|
||||
hideToolbar(event: FocusEvent | KeyboardEvent | MouseEvent, 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.
|
||||
blurRTE(event: FocusEvent): void {
|
||||
const doBlur = (event: FocusEvent) => {
|
||||
if (this.element.contains(document.activeElement)) {
|
||||
// Do not hide if clicked inside the editor area, except hideButton.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element.classList.remove('has-focus');
|
||||
|
||||
this.stopBubble(event);
|
||||
|
@ -740,6 +690,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
if (this.isPhone) {
|
||||
this.toolbarHidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
// There are many cases when focus is fired after blur, so we need to delay the blur action.
|
||||
this.blurTimeout = window.setTimeout(() => doBlur(event),300);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -753,19 +707,28 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
}
|
||||
|
||||
/**
|
||||
* Show the toolbar.
|
||||
* Focus editor when click the area and show toolbar.
|
||||
*
|
||||
* @param event Event.
|
||||
*/
|
||||
showToolbar(event: FocusEvent): void {
|
||||
this.updateToolbarButtons();
|
||||
focusRTE(event: Event): void {
|
||||
clearTimeout(this.blurTimeout);
|
||||
|
||||
if (this.rteEnabled) {
|
||||
this.editorElement?.focus();
|
||||
} else {
|
||||
this.textarea?.setFocus();
|
||||
}
|
||||
|
||||
this.element.classList.add('ion-touched');
|
||||
this.element.classList.remove('ion-untouched');
|
||||
this.element.classList.add('has-focus');
|
||||
|
||||
this.stopBubble(event);
|
||||
event && this.stopBubble(event);
|
||||
|
||||
this.editorElement?.focus();
|
||||
this.toolbarHidden = false;
|
||||
this.updateToolbarButtons();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -838,7 +801,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
* Update the number of toolbar buttons displayed.
|
||||
*/
|
||||
async updateToolbarButtons(): Promise<void> {
|
||||
if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent === null) {
|
||||
if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides ||
|
||||
this.toolbarHidden || this.element.offsetParent === null) {
|
||||
// Don't calculate if component isn't in current view, the calculations are wrong.
|
||||
return;
|
||||
}
|
||||
|
@ -847,10 +811,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
|
||||
// Cancel previous one, if any.
|
||||
this.buttonsDomPromise?.cancel();
|
||||
this.buttonsDomPromise = CoreDom.waitToBeInDOM(this.toolbar.nativeElement);
|
||||
this.buttonsDomPromise = CoreDom.waitToBeInDOM(this.toolbar?.nativeElement);
|
||||
await this.buttonsDomPromise;
|
||||
|
||||
const width = this.toolbar.nativeElement.getBoundingClientRect().width;
|
||||
const width = this.toolbar?.nativeElement.getBoundingClientRect().width;
|
||||
|
||||
if (length > 0 && width > length * this.toolbarButtonWidth) {
|
||||
this.swiperOpts.slidesPerView = length;
|
||||
|
@ -1068,7 +1032,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
const text = await CoreUtils.scanQR();
|
||||
|
||||
if (text) {
|
||||
this.editorElement?.focus(); // Make sure the editor is focused.
|
||||
this.focusRTE(event); // Make sure the editor is focused.
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
document.execCommand('insertText', false, text);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { Sqlite3Worker1Promiser, sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm';
|
||||
|
||||
/**
|
||||
|
@ -36,10 +37,13 @@ export class WasmSQLiteObject implements SQLiteObject {
|
|||
private name: string;
|
||||
private promisedPromiser: CorePromisedValue<Sqlite3Worker1Promiser>;
|
||||
private promiser: Sqlite3Worker1Promiser;
|
||||
protected logger: CoreLogger;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.promisedPromiser = new CorePromisedValue();
|
||||
this.logger = CoreLogger.getInstance('WasmSQLiteObject');
|
||||
|
||||
this.promiser = async (...args) => {
|
||||
const promiser = await this.promisedPromiser;
|
||||
|
||||
|
@ -50,7 +54,7 @@ export class WasmSQLiteObject implements SQLiteObject {
|
|||
/**
|
||||
* Delete the database.
|
||||
*/
|
||||
async delete(): Promise<any> {
|
||||
async delete(): Promise<void> {
|
||||
if (!this.promisedPromiser.isResolved()) {
|
||||
await this.open();
|
||||
}
|
||||
|
@ -61,20 +65,35 @@ export class WasmSQLiteObject implements SQLiteObject {
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async open(): Promise<any> {
|
||||
async open(): Promise<void> {
|
||||
const promiser = await new Promise<Sqlite3Worker1Promiser>((resolve) => {
|
||||
const _promiser = sqlite3Worker1Promiser(() => resolve(_promiser));
|
||||
});
|
||||
|
||||
await promiser('open', { filename: `file:${this.name}.sqlite3`, vfs: 'opfs' });
|
||||
const response = await promiser('config-get', {});
|
||||
const isEnabled = (response as any).result.opfsEnabled;
|
||||
if (!isEnabled) {
|
||||
this.logger.error('opfsEnabled flag is disabled. Reloading the page.');
|
||||
|
||||
// @TODO Fix Workaround for the issue with the opfsEnabled flag.
|
||||
// The flag gets disabled when opening a database, so we need to reload the page to make it work.
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
try {
|
||||
await promiser('open', { filename: `file:${this.name}.sqlite3`, vfs: 'opfs' });
|
||||
} catch (error) {
|
||||
this.logger.error(`Error opening database: ${this.name}. Details: ${error.result.message}`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
this.promisedPromiser.resolve(promiser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async close(): Promise<any> {
|
||||
async close(): Promise<void> {
|
||||
await this.promiser('close', {});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<core-spacer />
|
||||
<ion-item class="ion-text-wrap help">
|
||||
<ion-label>
|
||||
{{ 'core.search.filtercategories' | translate }}
|
||||
<p class="item-heading">{{ 'core.search.filtercategories' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
|
@ -37,7 +37,7 @@
|
|||
<core-spacer />
|
||||
<ion-item class="ion-text-wrap help">
|
||||
<ion-label>
|
||||
{{ 'core.search.filtercourses' | translate }}
|
||||
<p class="item-heading">{{ 'core.search.filtercourses' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
|
|
|
@ -449,7 +449,7 @@ export class CoreFileHelperProvider {
|
|||
Translate.instant('core.dontshowagain'),
|
||||
'checkbox',
|
||||
{ okText: okButton },
|
||||
{ cssClass: 'core-modal-force-on-top' },
|
||||
{ cssClass: 'core-alert-force-on-top' },
|
||||
);
|
||||
|
||||
if (dontShowAgain) {
|
||||
|
|
|
@ -35,3 +35,16 @@ ion-action-sheet {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File uploader.
|
||||
.action-sheet-button input.core-fileuploader-file-handler-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
opacity: 0;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,75 @@
|
|||
@keyframes scaleFrom0 {
|
||||
from {
|
||||
/* More performant than animating `width` */
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
ion-alert {
|
||||
--border-radius: var(--modal-radius);
|
||||
|
||||
&.md, &.ios {
|
||||
--max-width: 80%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
--max-width: 384px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-wrapper {
|
||||
overflow: auto;
|
||||
border-radius: var(--border-radius) !important;
|
||||
|
||||
button.alert-button.alert-button-role-destructive {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
user-select: text;
|
||||
flex-shrink: 0;
|
||||
|
||||
ion-card {
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.md .alert-button {
|
||||
// Remove padding from alert buttons, they already have min accessibility height.
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.alert-button.timed-button{
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
background-color: var(--primary-tint);
|
||||
animation: scaleFrom0 10s forwards linear;
|
||||
transform-origin: left;
|
||||
@include rtl() {
|
||||
transform-origin: right;
|
||||
}
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
&.core-alert-force-on-top {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
&.core-nohead .alert-head,
|
||||
.alert-head:empty {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ ion-button {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
&.button-disabled {
|
||||
opacity: var(--mdl-button-disabled-opacity) !important;
|
||||
}
|
||||
|
||||
&.button-outline {
|
||||
--border-width: var(--core-input-border-width);
|
||||
--border-color: var(--core-input-stroke);
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
|
||||
// Checkbox.
|
||||
ion-checkbox,
|
||||
input[type=checkbox] {
|
||||
--border-radius: 2px;
|
||||
--border-width: 2px;
|
||||
--outer-border-width: 2px;
|
||||
--border-style: solid;
|
||||
--size: 20px;
|
||||
|
||||
&:not(.ion-color) {
|
||||
--border-color-checked: var(--text-color);
|
||||
--checkbox-background-checked: var(--text-color);
|
||||
--checkmark-color: var(--contrast-background);
|
||||
}
|
||||
}
|
||||
|
||||
ion-checkbox {
|
||||
&.checkbox-disabled::part(label) {
|
||||
opacity: 0.8;
|
||||
opacity: var(--mdl-input-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.ios input[type=checkbox] {
|
||||
--outer-border-width: 1px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
ion-item.item-label-stacked ion-datetime-button {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-self: self-end;
|
||||
}
|
||||
|
||||
ion-datetime-button p {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
ion-datetime.datetime-disabled {
|
||||
opacity: var(--mdl-input-disabled-opacity) !important;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
// Hide close button because when present is read on voice over.
|
||||
ion-fab[core-fab] {
|
||||
ion-fab-button::part(close-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// The following 4 selectors can probably be removed after Ionic migration to 7+
|
||||
ion-fab.fab-horizontal-start {
|
||||
left: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
}
|
||||
|
||||
&[dir=rtl] ion-fab.fab-horizontal-start {
|
||||
right: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
left: unset
|
||||
}
|
||||
|
||||
ion-fab.fab-horizontal-end {
|
||||
right: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
}
|
||||
|
||||
&[dir=rtl] ion-fab.fab-horizontal-end {
|
||||
left: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
right: unset
|
||||
}
|
||||
|
||||
ion-content.has-collapsible-footer ion-fab {
|
||||
bottom: calc(var(--core-collapsible-footer-height, 0px) + 10px);
|
||||
@include core-transition(all, 200ms);
|
||||
}
|
||||
|
||||
ion-fab-button {
|
||||
--box-shadow: 0 3px 5px -1px rgb(0 0 0 / 20%), 0 6px 10px 0 rgb(0 0 0 / 14%), 0 1px 18px 0 rgb(0 0 0 / 12%);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
ion-input {
|
||||
&.input-disabled.md, &.input-disabled.ios {
|
||||
opacity: 0.8;
|
||||
opacity: var(--mdl-input-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@ ion-item.item {
|
|||
}
|
||||
|
||||
&.ion-valid,
|
||||
&.ion-invalid {
|
||||
&.ion-invalid,
|
||||
&.item-has-interactive-control {
|
||||
&.item-lines-default {
|
||||
--border-width: 0 0 1px 0;
|
||||
}
|
||||
|
@ -51,6 +52,12 @@ ion-item.item {
|
|||
&.item-has-interactive-control:focus-within {
|
||||
@include core-focus-outline();
|
||||
}
|
||||
|
||||
&.icon-margin-reduced {
|
||||
[slot=start] {
|
||||
@include margin-horizontal(null, var(--mdl-spacing-4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fake item.
|
||||
|
@ -71,7 +78,7 @@ div.fake-ion-item {
|
|||
font-size: var(--text-size);
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
@include padding(null, 16px, null, 16px);
|
||||
@include padding(null, var(--mdl-spacing-4), null, var(--mdl-spacing-4));
|
||||
@include margin(11px, null, 10px, null);
|
||||
|
||||
h1 {
|
||||
|
@ -183,10 +190,12 @@ ion-item .in-item {
|
|||
// Correctly inherit ion-text-wrap onto labels.
|
||||
.item > ion-label,
|
||||
.fake-ion-item,
|
||||
.item.ion-text-wrap > ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label)
|
||||
.item.ion-text-wrap ion-toggle::part(label),
|
||||
ion-toggle.ion-text-wrap::part(label) {
|
||||
.item > ion-checkbox::part(label),
|
||||
ion-checkbox::part(label),
|
||||
.item ion-toggle::part(label),
|
||||
ion-toggle::part(label),
|
||||
.item > ion-input > label,
|
||||
ion-input > label {
|
||||
core-format-text,
|
||||
core-format-text > *:not(pre) {
|
||||
white-space: nowrap;
|
||||
|
@ -200,7 +209,12 @@ ion-item > .in-item,
|
|||
.fake-ion-item.ion-text-wrap,
|
||||
.item.ion-text-wrap > ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label),
|
||||
ion-toggle.ion-text-wrap::part(label) {
|
||||
ion-toggle.ion-text-wrap::part(label),
|
||||
.item.ion-text-wrap > ion-input > label,
|
||||
ion-input.ion-text-wrap > label {
|
||||
white-space: normal;
|
||||
overflow: inherit;
|
||||
|
||||
core-format-text,
|
||||
core-format-text > *:not(pre) {
|
||||
white-space: normal;
|
||||
|
@ -208,16 +222,6 @@ ion-toggle.ion-text-wrap::part(label) {
|
|||
}
|
||||
}
|
||||
|
||||
.item.ion-text-wrap > ion-label,
|
||||
.item.ion-text-wrap ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label),
|
||||
.item.ion-text-wrap ion-radio::part(label),
|
||||
ion-radio.ion-text-wrap::part(label),
|
||||
.item.ion-text-wrap ion-toggle::part(label),
|
||||
ion-toggle.ion-text-wrap::part(label) {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
ion-item .core-input-errors-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -247,11 +251,11 @@ ion-item.item.item-file {
|
|||
}
|
||||
|
||||
&.item-directory ion-icon {
|
||||
@include margin-horizontal(0px, 16px);
|
||||
@include margin-horizontal(0px, var(--mdl-spacing-4));
|
||||
}
|
||||
|
||||
[slot=end] {
|
||||
@include margin-horizontal(16px, null);
|
||||
@include margin-horizontal(var(--mdl-spacing-4), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,6 +267,11 @@ ion-item.item.item-file {
|
|||
}
|
||||
}
|
||||
|
||||
// No highlight on RTE.
|
||||
ion-item.item-rte {
|
||||
--full-highlight-height: 0px !important;
|
||||
}
|
||||
|
||||
// Make links clickable when inside radio or checkbox items. Style part.
|
||||
@media (hover: hover) {
|
||||
ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
ion-modal {
|
||||
&.show-modal {
|
||||
@media only screen and (min-width: 768px) and (min-height: 600px) {
|
||||
--border-radius: var(--modal-radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.ion-datetime-button-overlay {
|
||||
--border-radius: var(--modal-radius);
|
||||
}
|
||||
|
||||
&.core-modal-lateral,
|
||||
&.core-modal-fullscreen {
|
||||
--border-radius: 0px;
|
||||
}
|
||||
|
||||
&.core-modal-no-background {
|
||||
--background: transparent;
|
||||
--box-shadow: none !important;
|
||||
pointer-events: none;
|
||||
|
||||
&::part(backdrop) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::part(content) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-sheet::part(handle) {
|
||||
background: var(--core-header-buttons-color);
|
||||
}
|
||||
|
||||
&.core-modal-fullscreen::part(content) {
|
||||
position: absolute;
|
||||
@include position(0 !important, null, null, 0 !important);
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
&.core-modal-transparent,
|
||||
&.core-modal-transparent-no-filter {
|
||||
|
||||
&::part(backdrop) {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
&::part(content) {
|
||||
backdrop-filter: none;
|
||||
--background: rgb(10 10 10 / 20%);
|
||||
|
||||
ion-content {
|
||||
--background: transparent !important;
|
||||
}
|
||||
position: absolute;
|
||||
@include position(0 !important, null, null, 0 !important);
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.core-modal-transparent {
|
||||
&::part(backdrop) {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
&::part(content) {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
}
|
||||
|
||||
&.core-modal-lateral {
|
||||
--ion-safe-area-left: 0px;
|
||||
--ion-safe-area-right: 0px;
|
||||
|
||||
&::part(content) {
|
||||
@include margin-horizontal(var(--modal-lateral-margin), null);
|
||||
|
||||
position: absolute;
|
||||
@include position(0 !important, 0 !important, 0 !important, unset !important);
|
||||
display: block;
|
||||
height: 100% !important;
|
||||
width: calc(100% - var(--modal-lateral-margin));
|
||||
max-width: calc(var(--modal-lateral-max-width));
|
||||
box-shadow: 0 28px 48px rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
&::part(backdrop) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-shadow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@each $breakpoint, $width in $screen-breakpoints {
|
||||
&.core-modal-lateral-#{$breakpoint} {
|
||||
--modal-lateral-max-width: #{$width};
|
||||
}
|
||||
}
|
||||
|
||||
&.core-password-modal {
|
||||
--border-radius: var(--mdl-shape-borderRadius-sm);
|
||||
--min-width: auto;
|
||||
--min-height: 300px;
|
||||
--width: 384px;
|
||||
--height: auto;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,63 @@
|
|||
|
||||
// Radio.
|
||||
ion-radio,
|
||||
input[type=radio],
|
||||
.select-alert .alert-radio-icon {
|
||||
--border-radius: 50%;
|
||||
--border-width: 2px;
|
||||
--outer-border-width: 2px;
|
||||
--border-style: solid;
|
||||
--size: 20px;
|
||||
|
||||
&:not(.ion-color) {
|
||||
--color: var(--text-color);
|
||||
--color-checked: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
.ios ion-radio,
|
||||
.ios input[type=radio],
|
||||
.select-alert.ios .alert-radio-icon {
|
||||
--border-width: 1px;
|
||||
--outer-border-width: 1px;
|
||||
}
|
||||
|
||||
.ios ion-radio {
|
||||
&::part(container) {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
|
||||
margin: 0px;
|
||||
border-radius: var(--border-radius);
|
||||
border-width: var(--outer-border-width);
|
||||
border-style: var(--border-style);
|
||||
border-color: var(--color);
|
||||
}
|
||||
|
||||
&::part(mark) {
|
||||
border-radius: var(--inner-border-radius);
|
||||
width: calc(50% + var(--outer-border-width));
|
||||
height: calc(50% + var(--outer-border-width));
|
||||
transform: scale3d(0, 0, 0);
|
||||
transition: transform 280ms cubic-bezier(.4, 0, .2, 1);
|
||||
background: var(--contrast-background);
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
&.radio-checked {
|
||||
&::part(container) {
|
||||
border-color: var(--color);
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
&::part(mark) {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-radio {
|
||||
&.radio-disabled::part(label) {
|
||||
opacity: 0.8;
|
||||
opacity: var(--mdl-input-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ ion-select {
|
|||
opacity: 1;
|
||||
}
|
||||
&.select-disabled {
|
||||
opacity: 0.8;
|
||||
opacity: var(--mdl-input-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,10 +111,6 @@ button,
|
|||
}
|
||||
}
|
||||
|
||||
ion-fab-button {
|
||||
--box-shadow: 0 3px 5px -1px rgb(0 0 0 / 20%), 0 6px 10px 0 rgb(0 0 0 / 14%), 0 1px 18px 0 rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
[role="button"],
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
|
@ -123,14 +119,14 @@ ion-fab-button {
|
|||
[disabled],
|
||||
[aria-disabled="true"] {
|
||||
cursor: default;
|
||||
opacity: .4;
|
||||
opacity: var(--mdl-button-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
cursor: default;
|
||||
opacity: .4;
|
||||
opacity: var(--mdl-button-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -188,72 +184,6 @@ div.core-iframe-network-error {
|
|||
left: -15%;
|
||||
}
|
||||
|
||||
ion-alert.core-nohead .alert-head,
|
||||
ion-alert .alert-head:empty {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@keyframes scaleFrom0 {
|
||||
from {
|
||||
/* More performant than animating `width` */
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
ion-alert .alert-button.timed-button{
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
background-color: var(--primary-tint);
|
||||
animation: scaleFrom0 10s forwards linear;
|
||||
transform-origin: left;
|
||||
@include rtl() {
|
||||
transform-origin: right;
|
||||
}
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
ion-alert {
|
||||
--border-radius: var(--modal-radius);
|
||||
|
||||
&.md, &.ios {
|
||||
--max-width: 80%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
--max-width: 384px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-wrapper {
|
||||
overflow: auto;
|
||||
border-radius: var(--border-radius) !important;
|
||||
|
||||
button.alert-button.alert-button-role-destructive {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
user-select: text;
|
||||
flex-shrink: 0;
|
||||
|
||||
ion-card {
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Ionic list.
|
||||
ion-list {
|
||||
padding: 0 !important;
|
||||
|
@ -375,96 +305,6 @@ body.core-iframe-fullscreen ion-router-outlet {
|
|||
}
|
||||
}
|
||||
|
||||
// Modals.
|
||||
.core-modal-fullscreen::part(content) {
|
||||
position: absolute;
|
||||
@include position(0 !important, null, null, 0 !important);
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.core-modal-transparent {
|
||||
|
||||
&::part(backdrop) {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&::part(content) {
|
||||
backdrop-filter: blur(12px);
|
||||
--background: rgb(10 10 10 / 20%);
|
||||
|
||||
ion-content {
|
||||
--background: transparent !important;
|
||||
}
|
||||
position: absolute;
|
||||
@include position(0 !important, null, null, 0 !important);
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.core-modal-force-on-top {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
.core-modal-lateral {
|
||||
--ion-safe-area-left: 0px;
|
||||
--ion-safe-area-right: 0px;
|
||||
|
||||
&::part(content) {
|
||||
@include margin-horizontal(var(--modal-lateral-margin), null);
|
||||
|
||||
position: absolute;
|
||||
@include position(0 !important, 0 !important, 0 !important, unset !important);
|
||||
display: block;
|
||||
height: 100% !important;
|
||||
width: calc(100% - var(--modal-lateral-margin));
|
||||
max-width: calc(var(--modal-lateral-max-width));
|
||||
box-shadow: 0 28px 48px rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
&::part(backdrop) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-shadow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.core-modal-transparent-no-filter {
|
||||
@extend .core-modal-transparent;
|
||||
&::part(backdrop) {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
&::part(content) {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@each $breakpoint, $width in $screen-breakpoints {
|
||||
.core-modal-lateral-#{$breakpoint} {
|
||||
--modal-lateral-max-width: #{$width};
|
||||
}
|
||||
}
|
||||
|
||||
.core-password-modal {
|
||||
--border-radius: var(--mdl-shape-borderRadius-sm);
|
||||
--min-width: auto;
|
||||
--min-height: 300px;
|
||||
--width: 384px;
|
||||
--height: auto;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden submit button.
|
||||
.core-submit-hidden-enter {
|
||||
position: absolute;
|
||||
|
@ -635,103 +475,12 @@ ion-content.limited-width > :not([slot]) {
|
|||
min-height: 100%;
|
||||
}
|
||||
|
||||
// Radio.
|
||||
ion-radio,
|
||||
input[type=radio],
|
||||
.select-alert .alert-radio-icon {
|
||||
--border-radius: 50%;
|
||||
--border-width: 2px;
|
||||
--outer-border-width: 2px;
|
||||
--border-style: solid;
|
||||
--size: 20px;
|
||||
|
||||
&:not(.ion-color) {
|
||||
--color: var(--text-color);
|
||||
--color-checked: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
.ios ion-radio,
|
||||
.ios input[type=radio],
|
||||
.select-alert.ios .alert-radio-icon {
|
||||
--border-width: 1px;
|
||||
--outer-border-width: 1px;
|
||||
}
|
||||
|
||||
.ios ion-radio {
|
||||
&::part(container) {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
|
||||
margin: 0px;
|
||||
border-radius: var(--border-radius);
|
||||
border-width: var(--outer-border-width);
|
||||
border-style: var(--border-style);
|
||||
border-color: var(--color);
|
||||
}
|
||||
|
||||
&::part(mark) {
|
||||
border-radius: var(--inner-border-radius);
|
||||
width: calc(50% + var(--outer-border-width));
|
||||
height: calc(50% + var(--outer-border-width));
|
||||
transform: scale3d(0, 0, 0);
|
||||
transition: transform 280ms cubic-bezier(.4, 0, .2, 1);
|
||||
background: var(--contrast-background);
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
&.radio-checked {
|
||||
&::part(container) {
|
||||
border-color: var(--color);
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
&::part(mark) {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Checkbox.
|
||||
ion-checkbox,
|
||||
input[type=checkbox] {
|
||||
--border-radius: 2px;
|
||||
--border-width: 2px;
|
||||
--outer-border-width: 2px;
|
||||
--border-style: solid;
|
||||
--size: 20px;
|
||||
|
||||
&:not(.ion-color) {
|
||||
--border-color-checked: var(--text-color);
|
||||
--checkbox-background-checked: var(--text-color);
|
||||
--checkmark-color: var(--contrast-background);
|
||||
}
|
||||
}
|
||||
|
||||
.ios input[type=checkbox] {
|
||||
--outer-border-width: 1px;
|
||||
}
|
||||
|
||||
ion-badge {
|
||||
line-height: 1.1;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--mdl-shape-borderRadius-lg);
|
||||
}
|
||||
|
||||
// File uploader.
|
||||
.action-sheet-button input.core-fileuploader-file-handler-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
opacity: 0;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.core-anchor, core-format-text a {
|
||||
color: var(--core-link-color);
|
||||
cursor: pointer;
|
||||
|
@ -811,37 +560,6 @@ textarea:not([core-auto-rows]) {
|
|||
height: 200px;
|
||||
}
|
||||
|
||||
// Hide close button because when present is read on voice over.
|
||||
ion-fab[core-fab] {
|
||||
ion-fab-button::part(close-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// The following 4 selectors can probably be removed after Ionic migration to 7+
|
||||
ion-fab.fab-horizontal-start {
|
||||
left: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
}
|
||||
|
||||
[dir=rtl] ion-fab.fab-horizontal-start {
|
||||
right: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
left: unset
|
||||
}
|
||||
|
||||
ion-fab.fab-horizontal-end {
|
||||
right: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
}
|
||||
|
||||
[dir=rtl] ion-fab.fab-horizontal-end {
|
||||
left: calc(10px + var(--ion-safe-area-right, 0px));
|
||||
right: unset
|
||||
}
|
||||
|
||||
ion-content.has-collapsible-footer ion-fab {
|
||||
bottom: calc(var(--core-collapsible-footer-height, 0px) + 10px);
|
||||
@include core-transition(all, 200ms);
|
||||
}
|
||||
|
||||
.core-media-adapt-width {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -856,10 +574,8 @@ audio.core-media-adapt-width {
|
|||
|
||||
// Disabled items.
|
||||
ion-item.item-disabled,
|
||||
ion-button.button-disabled,
|
||||
ion-item.item-interactive-disabled:not(.item-multiple-inputs) ion-label,
|
||||
ion-datetime.datetime-disabled {
|
||||
opacity: .65 !important;
|
||||
ion-item.item-interactive-disabled:not(.item-multiple-inputs) ion-label {
|
||||
opacity: var(--mdl-item-disabled-opacity) !important;
|
||||
}
|
||||
|
||||
ion-item-divider.item,
|
||||
|
@ -893,7 +609,7 @@ ion-input input,
|
|||
ion-textarea,
|
||||
core-rich-text-editor {
|
||||
--placeholder-color: var(--ion-placeholder-color);
|
||||
--placeholder-opacity: .65;
|
||||
--placeholder-opacity: var(--mdl-placeholder-opacity);
|
||||
}
|
||||
|
||||
// Disable scroll on parent ion contents to enabled PTR on the ones inside the splitview. See split-view component for more info.
|
||||
|
@ -1057,37 +773,6 @@ video::-webkit-media-text-track-display {
|
|||
white-space: normal !important;
|
||||
}
|
||||
|
||||
ion-modal {
|
||||
&.show-modal {
|
||||
@media only screen and (min-width: 768px) and (min-height: 600px) {
|
||||
--border-radius: var(--modal-radius);
|
||||
}
|
||||
}
|
||||
&.ion-datetime-button-overlay {
|
||||
--border-radius: var(--modal-radius);
|
||||
}
|
||||
|
||||
&.core-modal-lateral,
|
||||
&.core-modal-fullscreen {
|
||||
--border-radius: 0px;
|
||||
}
|
||||
|
||||
&.core-modal-no-background {
|
||||
--background: transparent;
|
||||
--box-shadow: none !important;
|
||||
pointer-events: none;
|
||||
|
||||
&::part(backdrop) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-sheet::part(handle) {
|
||||
background: var(--core-header-buttons-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* This is to solve popver issue in chrome 114
|
||||
* For more info see: https://github.com/ionic-team/ionic-framework/issues/27599
|
||||
|
@ -1113,18 +798,6 @@ ion-modal {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
// Ion Datetime
|
||||
ion-item.item-label-stacked ion-datetime-button {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-self: self-end;
|
||||
}
|
||||
|
||||
ion-datetime-button p {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.x-scrollable {
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
|
|
|
@ -140,6 +140,12 @@ html {
|
|||
--mdl-shadow-boxShadow-16: 0px 7px 8px 0px rgb(var(--mdl-shadow-boxShadowColor) / 20%),0px 5px 22px 0px rgb(var(--mdl-shadow-boxShadowColor) / 12%),0px 12px 17px 0px rgb(var(--mdl-shadow-boxShadowColor) / 14%);
|
||||
--mdl-shadow-boxShadow-24: 0px 11px 15px 0px rgb(var(--mdl-shadow-boxShadowColor) / 20%), 0px 9px 46px 0px rgb(var(--mdl-shadow-boxShadowColor) / 12%), 0px 24px 38px 0px rgb(var(--mdl-shadow-boxShadowColor) / 14%);
|
||||
|
||||
// ***** OPACITY category ***** //
|
||||
--mdl-button-disabled-opacity: 0.6;
|
||||
--mdl-input-disabled-opacity: 0.6;
|
||||
--mdl-item-disabled-opacity: 0.4;
|
||||
--mdl-placeholder-opacity: 0.6;
|
||||
|
||||
// ***** ACCESSIBILITY ***** //
|
||||
--a11y-sizing-minTargetSize: 44px;
|
||||
--a11y-shadow-focus-borderWidth: 2px;
|
||||
|
|
|
@ -36,11 +36,14 @@ html {
|
|||
@import "components/ion-checkbox.scss";
|
||||
@import "components/ion-chip.scss";
|
||||
@import "components/ion-content.scss";
|
||||
@import "components/ion-datetime.scss";
|
||||
@import "components/ion-fab.scss";
|
||||
@import "components/ion-header.scss";
|
||||
@import "components/ion-icon.scss";
|
||||
@import "components/ion-input.scss";
|
||||
@import "components/ion-item.scss";
|
||||
@import "components/ion-item-divider.scss";
|
||||
@import "components/ion-modal.scss";
|
||||
@import "components/ion-loading.scss";
|
||||
@import "components/ion-note.scss";
|
||||
@import "components/ion-popover.scss";
|
||||
|
|
Loading…
Reference in New Issue