Merge pull request #4062 from crazyserver/MOBILE-4600

Mobile 4600
main
Dani Palou 2024-05-27 15:23:46 +02:00 committed by GitHub
commit 0e599a5363
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 3122 additions and 461 deletions

View File

@ -555,6 +555,7 @@
"addon.mod_data.authorfirstname": "data", "addon.mod_data.authorfirstname": "data",
"addon.mod_data.authorlastname": "data", "addon.mod_data.authorlastname": "data",
"addon.mod_data.confirmdeleterecord": "data", "addon.mod_data.confirmdeleterecord": "data",
"addon.mod_data.datemodified": "data",
"addon.mod_data.descending": "data", "addon.mod_data.descending": "data",
"addon.mod_data.disapprove": "data", "addon.mod_data.disapprove": "data",
"addon.mod_data.edittagsnotsupported": "local_moodlemobileapp", "addon.mod_data.edittagsnotsupported": "local_moodlemobileapp",
@ -2548,6 +2549,7 @@
"core.storingfiles": "local_moodlemobileapp", "core.storingfiles": "local_moodlemobileapp",
"core.strftimedate": "langconfig", "core.strftimedate": "langconfig",
"core.strftimedatefullshort": "langconfig", "core.strftimedatefullshort": "langconfig",
"core.strftimedatemonthabbr": "langconfig",
"core.strftimedateshort": "langconfig", "core.strftimedateshort": "langconfig",
"core.strftimedatetime": "langconfig", "core.strftimedatetime": "langconfig",
"core.strftimedatetimeshort": "langconfig", "core.strftimedatetimeshort": "langconfig",

View File

@ -74,7 +74,7 @@ export abstract class AddonModDataFieldPluginBaseComponent implements OnInit, On
} }
/** /**
* Component being changed. * @inheritdoc
*/ */
ngOnChanges(changes: { [name: string]: SimpleChange }): void { ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if ((this.showMode || this.listMode) && changes.value) { if ((this.showMode || this.listMode) && changes.value) {

View File

@ -34,8 +34,12 @@
<core-comments *ngIf="action === 'comments' && mode === 'list'" contextLevel="module" [instanceId]="database.coursemodule" <core-comments *ngIf="action === 'comments' && mode === 'list'" contextLevel="module" [instanceId]="database.coursemodule"
component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="database.course" /> component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="database.course" />
<span *ngIf="action === 'timeadded'">{{ entry.timecreated * 1000 | coreFormatDate }}</span> <span *ngIf="action === 'timeadded'" [title]="entry.timecreated * 1000 | coreFormatDate">
<span *ngIf="action === 'timemodified'">{{ entry.timemodified * 1000 | coreFormatDate }}</span> {{ entry.timecreated * 1000 | coreFormatDate: 'strftimedatemonthabbr' }}
</span>
<span *ngIf="action === 'timemodified'" [title]="entry.timemodified * 1000 | coreFormatDate">
{{ entry.timemodified * 1000 | coreFormatDate: 'strftimedatemonthabbr' }}
</span>
<core-user-avatar *ngIf="action === 'userpicture'" [user]="entry" slot="start" [courseId]="database.course" [userId]="entry.userid" <core-user-avatar *ngIf="action === 'userpicture'" [user]="entry" slot="start" [courseId]="database.course" [userId]="entry.userid"
[profileUrl]="userPicture" /> [profileUrl]="userPicture" />

View File

@ -80,22 +80,7 @@
<core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports" /> <core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports" />
</div> </div>
<ion-grid *ngIf="search.page > 0 || hasNextPage">
<ion-row class="ion-align-items-center">
<ion-col *ngIf="search.page > 0">
<ion-button expand="block" fill="outline" (click)="searchEntries(search.page - 1)">
<ion-icon name="fas-chevron-left" slot="start" aria-hidden="true" />
{{ 'core.previous' | translate }}
</ion-button>
</ion-col>
<ion-col *ngIf="hasNextPage">
<ion-button expand="block" (click)="searchEntries(search.page + 1)">
{{ 'core.next' | translate }}
<ion-icon name="fas-chevron-right" slot="end" aria-hidden="true" />
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="isEmpty && !search.searching" icon="fas-database" [message]="'addon.mod_data.norecords' | translate" /> <core-empty-box *ngIf="isEmpty && !search.searching" icon="fas-database" [message]="'addon.mod_data.norecords' | translate" />
@ -104,9 +89,28 @@
<button class="as-link" (click)="searchReset($event)">{{ 'addon.mod_data.resetsettings' | translate}}</button> <button class="as-link" (click)="searchReset($event)">{{ 'addon.mod_data.resetsettings' | translate}}</button>
</core-empty-box> </core-empty-box>
<div collapsible-footer appearOnBottom *ngIf="!showLoading" slot="fixed">
<ion-grid *ngIf="search.page > 0 || hasNextPage">
<ion-row class="ion-align-items-center">
<ion-col *ngIf="search.page > 0">
<ion-button expand="block" fill="outline" (click)="searchEntries(search.page - 1)">
<ion-icon name="fas-chevron-left" slot="start" aria-hidden="true" />
{{ 'core.previous' | translate }}
</ion-button>
</ion-col>
<ion-col *ngIf="hasNextPage">
<ion-button expand="block" (click)="searchEntries(search.page + 1)">
{{ 'core.next' | translate }}
<ion-icon name="fas-chevron-right" slot="end" aria-hidden="true" />
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
<core-course-module-navigation [courseId]="courseId" [currentModuleId]="module.id" />
</div>
</core-loading> </core-loading>
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id" />
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
<ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate"> <ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate">

View File

@ -3,25 +3,32 @@
// Edit and search modal. // Edit and search modal.
:host { :host {
::ng-deep {
table {
width: 100%;
}
td {
vertical-align: top;
}
}
.addon-data-advanced-search { .addon-data-advanced-search {
padding: 16px; padding: 16px;
width: 100%; width: 100%;
} }
.addon-data-contents form, .addon-data-edit-entry form,
form .addon-data-advanced-search { form .addon-data-advanced-search {
background-color: var(--ion-item-background); background-color: var(--ion-item-background);
::ng-deep { ::ng-deep {
table {
width: 100%;
tbody {
display: block;
}
td {
vertical-align: top;
}
}
.edit-field,
.search-field {
border-bottom: 1px solid var(--stroke);
}
.has-errors { .has-errors {
.input-highlight, .input-highlight,
.select-highlight, .select-highlight,

View File

@ -16,17 +16,18 @@ $grid-column-paddings: (
--border-color: var(--stroke); --border-color: var(--stroke);
} }
.addon-data-edit-entry { .addon-data-contents {
overflow: visible; overflow: visible;
white-space: normal; white-space: normal;
word-break: break-word; word-break: break-word;
padding: 16px; padding: 16px;
background-color: var(--ion-item-background);
border-bottom: 1px solid var(--border-color);
::ng-deep { ::ng-deep {
table, tbody {
@import "theme/components/moodle.scss";
@import "theme/components/bootstrap/bootstrap_database.scss";
table {
display: block; display: block;
} }

View File

@ -1,16 +1,17 @@
<span *ngIf="inputMode && form && searchFields" [formGroup]="form"> <span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span> <span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-datetime-button datetime="datetime" /> <ion-datetime-button datetime="datetime" />
<ion-modal [keepContentsMounted]="true"> <ion-modal [keepContentsMounted]="true">
<ng-template> <ng-template>
<ion-datetime id="datetime" [formControlName]="'f_'+field.id" [max]="maxDate" [min]="minDate" <ion-datetime id="datetime" [formControlName]="'f_'+field.id" [max]="maxDate" [min]="minDate"
[disabled]="searchMode && !searchFields['f_'+field.id+'_z']" presentation="date" [showDefaultButtons]="true" /> [disabled]="searchMode && searchFields && !searchFields['f_'+field.id+'_z']" presentation="date"
[showDefaultButtons]="true" />
</ng-template> </ng-template>
</ion-modal> </ion-modal>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" /> <core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
<ion-item *ngIf="searchMode" class="ion-text-wrap"> <ion-item *ngIf="searchMode && searchFields" class="ion-text-wrap">
<ion-checkbox [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields['f_'+field.id+'_z']"> <ion-checkbox [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields['f_'+field.id+'_z']">
{{ 'addon.mod_data.usedate' | translate }} {{ 'addon.mod_data.usedate' | translate }}
</ion-checkbox> </ion-checkbox>

View File

@ -9,6 +9,7 @@
"authorfirstname": "First name", "authorfirstname": "First name",
"authorlastname": "Last name", "authorlastname": "Last name",
"confirmdeleterecord": "Are you sure you want to delete this entry?", "confirmdeleterecord": "Are you sure you want to delete this entry?",
"datemodified": "Last edited:",
"descending": "Descending", "descending": "Descending",
"disapprove": "Undo approval", "disapprove": "Undo approval",
"edittagsnotsupported": "Sorry, editing tags is not supported by the app.", "edittagsnotsupported": "Sorry, editing tags is not supported by the app.",

View File

@ -247,7 +247,7 @@ export class AddonModDataHelperProvider {
continue; continue;
} }
if (action == AddonModDataAction.MOREURL) { if (action === AddonModDataAction.MOREURL) {
// Render more url directly because it can be part of an HTML attribute. // Render more url directly because it can be part of an HTML attribute.
template = template.replace( template = template.replace(
replaceRegex, replaceRegex,
@ -255,10 +255,26 @@ export class AddonModDataHelperProvider {
); );
continue; continue;
} else if (action == 'approvalstatus') { } else if (action === AddonModDataAction.APPROVALSTATUS) {
template = template.replace( template = template.replace(
replaceRegex, replaceRegex,
Translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')), entry.approved
? ''
: `<ion-badge color="warning">${Translate.instant('addon.mod_data.notapproved')}</ion-badge>`,
);
continue;
} else if (action === AddonModDataAction.APPROVALSTATUSCLASS) {
template = template.replace(
replaceRegex,
entry.approved ? 'approved' : 'notapproved',
);
continue;
} else if (action === AddonModDataAction.ID) {
template = template.replace(
replaceRegex,
entry.id.toString(),
); );
continue; continue;
@ -460,6 +476,7 @@ export class AddonModDataHelperProvider {
timeadded: true, timeadded: true,
timemodified: true, timemodified: true,
tags: true, tags: true,
id: true,
edit: entry.canmanageentry && !entry.deleted, // This already checks capabilities and readonly period. edit: entry.canmanageentry && !entry.deleted, // This already checks capabilities and readonly period.
delete: entry.canmanageentry, delete: entry.canmanageentry,
@ -467,6 +484,7 @@ export class AddonModDataHelperProvider {
disapprove: database.approval && accessInfo.canapprove && entry.approved && !entry.deleted, disapprove: database.approval && accessInfo.canapprove && entry.approved && !entry.deleted,
approvalstatus: database.approval, approvalstatus: database.approval,
approvalstatusclass: database.approval,
comments: database.comments, comments: database.comments,
actionsmenu: entry.canmanageentry actionsmenu: entry.canmanageentry
@ -504,84 +522,165 @@ export class AddonModDataHelperProvider {
/** /**
* Returns the default template of a certain type. * Returns the default template of a certain type.
* *
* Based on Moodle function data_generate_default_template.
*
* @param type Type of template. * @param type Type of template.
* @param fields List of database fields. * @param fields List of database fields.
* @returns Template HTML. * @returns Template HTML.
*/ */
getDefaultTemplate(type: AddonModDataTemplateType, fields: AddonModDataField[]): string { protected getDefaultTemplate(type: AddonModDataTemplateType, fields: AddonModDataField[]): string {
if (type == AddonModDataTemplateType.LIST_HEADER || type == AddonModDataTemplateType.LIST_FOOTER) { switch (type) {
return ''; case AddonModDataTemplateType.LIST:
return this.getDefaultListTemplate(fields);
case AddonModDataTemplateType.SINGLE:
return this.getDefaultSingleTemplate(fields);
case AddonModDataTemplateType.SEARCH:
return this.getDefaultSearchTemplate(fields);
case AddonModDataTemplateType.ADD:
return this.getDefaultAddTemplate(fields);
} }
return '';
}
/**
* Returns the default template for the list view.
*
* @param fields List of database fields.
* @returns Template HTML.
*/
protected getDefaultListTemplate(fields: AddonModDataField[]): string {
const html: string[] = []; const html: string[] = [];
if (type == AddonModDataTemplateType.LIST) { html.push(`<ion-card class="defaulttemplate-listentry">
html.push('##delcheck##<br />'); <ion-item class="ion-text-wrap" lines="full">
} ##userpicture##
<ion-label>
<p class="item-heading">##user##</p>
<p class="data-timeinfo">##timeadded##</p>
<p class="data-timeinfo">
<strong>${Translate.instant('addon.mod_data.datemodified')}</strong>&nbsp;##timemodified##
</p>
</ion-label>
<div slot="end" class="ion-text-end">
##actionsmenu##
<p class="ion-text-end ##approvalstatusclass##">##approvalstatus##</p>
</div>
</ion-item>
html.push( <ion-item class="ion-text-wrap defaulttemplate-list-body"><ion-label>`);
'<div class="defaulttemplate">',
'<table class="mod-data-default-template ##approvalstatus##">',
'<tbody>',
);
fields.forEach((field) => { fields.forEach((field) => {
html.push( html.push(`
'<tr class="">', <ion-row class="ion-margin-vertical ion-align-items-start ion-justify-content-start">
'<td class="template-field cell c0" style="">', <ion-col size="4" size-lg="3"><strong>${field.name}</strong></ion-col>
field.name, <ion-col size="8" size-lg="9">[[${field.name}]]</ion-col>
': </td>', </ion-row>`);
'<td class="template-token cell c1 lastcol" style="">[[',
field.name,
']]</td>',
'</tr>',
);
}); });
if (type == AddonModDataTemplateType.LIST) { html.push('##tags##</ion-label></ion-item></ion-card>');
html.push(
'<tr class="lastrow">',
'<td class="controls template-field cell c0 lastcol" style="" colspan="2">',
'##actionsmenu## ##edit## ##more## ##delete## ##approve## ##disapprove## ##export##',
'</td>',
'</tr>',
);
} else if (type == AddonModDataTemplateType.SINGLE) {
html.push(
'<tr class="lastrow">',
'<td class="controls template-field cell c0 lastcol" style="" colspan="2">',
'##actionsmenu## ##edit## ##delete## ##approve## ##disapprove## ##export##',
'</td>',
'</tr>',
);
} else if (type == AddonModDataTemplateType.SEARCH) {
html.push(
'<tr class="searchcontrols">',
'<td class="template-field cell c0" style="">Author first name: </td>',
'<td class="template-token cell c1 lastcol" style="">##firstname##</td>',
'</tr>',
'<tr class="searchcontrols lastrow">',
'<td class="template-field cell c0" style="">Author surname: </td>',
'<td class="template-token cell c1 lastcol" style="">##lastname##</td>',
'</tr>',
);
}
html.push(
'</tbody>',
'</table>',
'</div>',
);
if (type == AddonModDataTemplateType.LIST) {
html.push('<hr />');
}
return html.join(''); return html.join('');
} }
/**
* Returns the default template for the add view.
*
* @param fields List of database fields.
* @returns Template HTML.
*/
protected getDefaultAddTemplate(fields: AddonModDataField[]): string {
const html: string[] = [];
html.push('<div class="defaulttemplate-addentry">');
fields.forEach((field) => {
html.push(`
<div class="ion-text-wrap edit-field">
<p><strong>${field.name}</strong></p>
[[${field.name}]]
</div>`);
});
html.push('##otherfields## ##tags##</div>');
return html.join('');
}
/**
* Returns the default template for the single view.
*
* @param fields List of database fields.
* @returns Template HTML.
*/
protected getDefaultSingleTemplate(fields: AddonModDataField[]): string {
const html: string[] = [];
html.push(`<div class="defaulttemplate-single">
<div class="defaulttemplate-single-body">
<ion-item class="ion-text-wrap" lines="full">
##userpicture##
<ion-label>
<p class="item-heading">##user##</p>
<p class="data-timeinfo">##timeadded##</p>
<p class="data-timeinfo">
<strong>${Translate.instant('addon.mod_data.datemodified')}</strong>&nbsp;##timemodified##
</p>
</ion-label>
<div slot="end" class="ion-text-end">
##actionsmenu##
<p class="ion-text-end ##approvalstatusclass##">##approvalstatus##</p>
</div>
</ion-item>`);
fields.forEach((field) => {
html.push(`
<ion-item class="ion-text-wrap" lines="none"><ion-label>
<p class="item-heading"><strong>${field.name}</strong></p>
<p>[[${field.name}]]</p>
</ion-label></ion-item>`);
});
html.push('##otherfields## ##tags##</ion-label></ion-item></div></div>');
return html.join('');
}
/**
* Returns the default template for the search view.
*
* @param fields List of database fields.
* @returns Template HTML.
*/
protected getDefaultSearchTemplate(fields: AddonModDataField[]): string {
const html: string[] = [];
html.push('<div class="defaulttemplate-asearch">');
html.push(`
<div class="ion-text-wrap search-field">
<p><strong>${Translate.instant('addon.mod_data.authorfirstname')}</strong></p>
##firstname##
</div>`);
html.push(`
<div class="ion-text-wrap search-field">
<p><strong>${Translate.instant('addon.mod_data.authorlastname')}</strong></p>
##lastname##
</div>`);
fields.forEach((field) => {
html.push(`
<div class="ion-text-wrap search-field">
<p><strong>${field.name}</strong></p>
[[${field.name}]]
</div>`);
});
html.push('##tags##</div>');
return html.join('');
}
/** /**
* Retrieve the entered data in the edit form. * Retrieve the entered data in the edit form.
* We don't use ng-model because it doesn't detect changes done by JavaScript. * We don't use ng-model because it doesn't detect changes done by JavaScript.

View File

@ -61,9 +61,11 @@ export enum AddonModDataAction {
TIMEMODIFIED = 'timemodified', TIMEMODIFIED = 'timemodified',
TAGS = 'tags', TAGS = 'tags',
APPROVALSTATUS = 'approvalstatus', APPROVALSTATUS = 'approvalstatus',
APPROVALSTATUSCLASS = 'approvalstatusclass',
DELCHECK = 'delcheck', // Unused. DELCHECK = 'delcheck', // Unused.
EXPORT = 'export', // Unused. EXPORT = 'export', // Unused.
ACTIONSMENU = 'actionsmenu', ACTIONSMENU = 'actionsmenu',
ID = 'id',
} }
export enum AddonModDataTemplateType { export enum AddonModDataTemplateType {

View File

@ -59,7 +59,8 @@ Feature: Users can manage entries in database activities
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
| Description | Moodle Cloud | | Description | Moodle Cloud |
And I press "Save" near "Web links" in the app And I press "Save" near "Web links" in the app
And I press "Show more" near "Moodle community site" in the app And I press "Actions menu" near "Moodle community site" in the app
And I press "Show more" in the app
Then I should find "Moodle community site" in the app Then I should find "Moodle community site" in the app
And I should not find "Comments" in the app And I should not find "Comments" in the app
And I should be able to press "Previous" in the app And I should be able to press "Previous" in the app
@ -83,14 +84,16 @@ Feature: Users can manage entries in database activities
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Description | Moodle community site | | Description | Moodle community site |
And I press "Save" near "Data with comments" in the app And I press "Save" near "Data with comments" in the app
And I press "Show more" near "Moodle community site" in the app And I press "Actions menu" near "Moodle community site" in the app
And I press "Show more" in the app
Then I should find "Moodle community site" in the app Then I should find "Moodle community site" in the app
And I should find "Comments" in the app And I should find "Comments" in the app
Given the following config values are set as admin: Given the following config values are set as admin:
| usecomments | 0 | | usecomments | 0 |
And I entered the data activity "Data with comments" on course "Course 1" as "student1" in the app And I entered the data activity "Data with comments" on course "Course 1" as "student1" in the app
When I press "Show more" near "Moodle community site" in the app When I press "Actions menu" near "Moodle community site" in the app
And I press "Show more" in the app
Then I should not find "Comments" in the app Then I should not find "Comments" in the app
But the following events should have been logged for "student1" in the app: But the following events should have been logged for "student1" in the app:
| name | activity | activityname | course | | name | activity | activityname | course |
@ -106,9 +109,11 @@ Feature: Users can manage entries in database activities
And I press "Save" near "Web links" in the app And I press "Save" near "Web links" in the app
And I entered the course "Course 1" as "student2" in the app And I entered the course "Course 1" as "student2" in the app
When I press "Web links" near "General" in the app When I press "Web links" near "General" in the app
And I press "Actions menu" in the app
Then "Edit" "link" should not exist Then "Edit" "link" should not exist
And "Delete" "link" should not exist And "Delete" "link" should not exist
And I press "Show more" in the app When I press "Show more" in the app
And "Actions menu" "link" should not exist
And "Edit" "link" should not exist And "Edit" "link" should not exist
And "Delete" "link" should not exist And "Delete" "link" should not exist
@ -121,7 +126,8 @@ Feature: Users can manage entries in database activities
And I press "Save" near "Web links" in the app And I press "Save" near "Web links" in the app
# Edit the entry from list view. # Edit the entry from list view.
When I press "Edit" in the app When I press "Actions menu" in the app
And I press "Edit" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
| Description | Moodle Cloud | | Description | Moodle Cloud |
@ -132,11 +138,13 @@ Feature: Users can manage entries in database activities
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
# Delete the entry from list view. # Delete the entry from list view.
When I press "Delete" in the app When I press "Actions menu" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Cancel" in the app And I press "Cancel" in the app
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
When I press "Delete" in the app When I press "Actions menu" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should not find "Moodle Cloud" in the app And I should not find "Moodle Cloud" in the app
@ -149,7 +157,9 @@ Feature: Users can manage entries in database activities
And I press "Save" near "Web links" in the app And I press "Save" near "Web links" in the app
# Edit the entry from single view. # Edit the entry from single view.
When I press "Show more" in the app When I press "Actions menu" in the app
And I press "Show more" in the app
And I press "Actions menu" in the app
And I press "Edit" in the app And I press "Edit" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
@ -161,11 +171,13 @@ Feature: Users can manage entries in database activities
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
# Delete the entry from list view. # Delete the entry from list view.
When I press "Delete" in the app When I press "Actions menu" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Cancel" in the app And I press "Cancel" in the app
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
When I press "Delete" in the app When I press "Actions menu" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should not find "Moodle Cloud" in the app And I should not find "Moodle Cloud" in the app
@ -190,7 +202,8 @@ Feature: Users can manage entries in database activities
And I should find "Moodle community site" in the app And I should find "Moodle community site" in the app
# Edit the entry from list view. # Edit the entry from list view.
When I press "Edit" near "Moodle community site" in the app When I press "Actions menu" near "Moodle community site" in the app
And I press "Edit" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
| Description | Moodle Cloud | | Description | Moodle Cloud |
@ -201,19 +214,23 @@ Feature: Users can manage entries in database activities
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
# Delete the entry from list view. # Delete the entry from list view.
When I press "Delete" near "Moodle Cloud" in the app When I press "Actions menu" near "Moodle Cloud" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Cancel" in the app And I press "Cancel" in the app
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
When I press "Delete" near "Moodle Cloud" in the app When I press "Actions menu" near "Moodle Cloud" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should not find "Moodle Cloud" in the app And I should not find "Moodle Cloud" in the app
# Edit the entry from single view. # Edit the entry from single view.
When I press "Show more" in the app When I press "Actions menu" in the app
And I press "Show more" in the app
And I should find "https://telegram.org/" in the app And I should find "https://telegram.org/" in the app
And I should find "Telegram" in the app And I should find "Telegram" in the app
And I press "Actions menu" in the app
And I press "Edit" in the app And I press "Edit" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
@ -225,11 +242,13 @@ Feature: Users can manage entries in database activities
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
# Delete the entry from single view. # Delete the entry from single view.
When I press "Delete" in the app When I press "Actions menu" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Cancel" in the app And I press "Cancel" in the app
And I should find "Moodle Cloud" in the app And I should find "Moodle Cloud" in the app
When I press "Delete" in the app When I press "Actions menu" in the app
And I press "Delete" in the app
Then I should find "Are you sure you want to delete this entry?" in the app Then I should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should not find "Moodle Cloud" in the app And I should not find "Moodle Cloud" in the app
@ -250,5 +269,5 @@ Feature: Users can manage entries in database activities
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Number | 0 | | Number | 0 |
And I press "Save" near "Number DB" in the app And I press "Save" near "Number DB" in the app
Then I should find "0" near "Number:" in the app Then I should find "0" in the app
But I should not find "Save" in the app But I should not find "Save" in the app

View File

@ -61,7 +61,8 @@ Feature: Users can store entries in database activities when offline and sync wh
And I wait until the page is ready And I wait until the page is ready
And I close the popup in the app And I close the popup in the app
And I switch network connection to offline And I switch network connection to offline
When I press "Edit" in the app When I press "Actions menu" in the app
And I press "Edit" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
| Description | Moodle Cloud | | Description | Moodle Cloud |
@ -83,6 +84,7 @@ Feature: Users can store entries in database activities when offline and sync wh
And I press "Refresh" in the app And I press "Refresh" in the app
And I wait until the page is ready And I wait until the page is ready
And I switch network connection to offline And I switch network connection to offline
And I press "Actions menu" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should find "Are you sure you want to delete this entry?" in the app And I should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
@ -111,12 +113,14 @@ Feature: Users can store entries in database activities when offline and sync wh
And I wait until the page is ready And I wait until the page is ready
And I close the popup in the app And I close the popup in the app
When I switch network connection to offline When I switch network connection to offline
And I press "Actions menu" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should find "Are you sure you want to delete this entry?" in the app And I should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I should find "https://moodle.org/" in the app And I should find "https://moodle.org/" in the app
And I should find "Moodle community site" in the app And I should find "Moodle community site" in the app
And I should find "This Database has offline data to be synchronised" in the app And I should find "This Database has offline data to be synchronised" in the app
And I press "Actions menu" in the app
And I press "Restore" in the app And I press "Restore" in the app
And I press the back button in the app And I press the back button in the app
And I switch network connection to wifi And I switch network connection to wifi

View File

@ -33,6 +33,7 @@ Feature: Test basic usage of comments in app
And I press "Add entries" in the app And I press "Add entries" in the app
And I set the field "Test field name" to "Test" in the app And I set the field "Test field name" to "Test" in the app
And I press "Save" in the app And I press "Save" in the app
And I press "Actions menu" in the app
And I press "Show more" in the app And I press "Show more" in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I set the field "Add a comment..." to "comment test teacher" in the app And I set the field "Add a comment..." to "comment test teacher" in the app
@ -45,6 +46,7 @@ Feature: Test basic usage of comments in app
# Create and delete comments as a student # Create and delete comments as a student
Given I entered the data activity "Data" on course "Course 1" as "student1" in the app Given I entered the data activity "Data" on course "Course 1" as "student1" in the app
And I press "Actions menu" in the app
And I press "Show more" in the app And I press "Show more" in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I set the field "Add a comment..." to "comment test student" in the app And I set the field "Add a comment..." to "comment test student" in the app
@ -70,6 +72,7 @@ Feature: Test basic usage of comments in app
And I press "Add entries" in the app And I press "Add entries" in the app
And I set the field "Test field name" to "Test" in the app And I set the field "Test field name" to "Test" in the app
And I press "Save" in the app And I press "Save" in the app
And I press "Actions menu" in the app
And I press "Show more" in the app And I press "Show more" in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I switch network connection to offline And I switch network connection to offline

View File

@ -312,6 +312,7 @@
"startingtime": "Starting time: {{$a}}", "startingtime": "Starting time: {{$a}}",
"storingfiles": "Storing files", "storingfiles": "Storing files",
"strftimedate": "%d %B %Y", "strftimedate": "%d %B %Y",
"strftimedatemonthabbr": "%d %b %Y",
"strftimedatefullshort": "%d/%m/%y", "strftimedatefullshort": "%d/%m/%y",
"strftimedateshort": "%d %B", "strftimedateshort": "%d %B",
"strftimedatetime": "%d %B %Y, %I:%M %p", "strftimedatetime": "%d %B %Y, %I:%M %p",

View File

@ -0,0 +1,73 @@
// Atto styles
// -------------------------
.atto_image_preview {
width: 100%;
height: 100%;
margin-left: auto;
margin-right: auto;
}
.atto_image_preview_box {
max-height: 200px;
margin-bottom: 1em;
overflow: auto;
}
.editor_atto_content img {
cursor: pointer;
}
.atto_image_size {
display: inline-block;
}
.atto_image_size input[type=checkbox] {
@include margin(null, 1em, null, 1em);
}
.atto_image_size input[type=text] {
width: 3em;
}
.atto_image_size label {
display: inline-block;
}
.atto_image_button_text-top,
.atto_image_button_middle,
.atto_image_button_text-bottom,
.atto_image_button_left,
.atto_image_button_right {
vertical-align: middle;
max-width: 100%;
display: inline-block;
margin: 0 0.5em;
&.img-responsive {
/* If the image is display: block then linking the image to URLs won't work. */
/*display: inline-block;*/
max-width: 100%;
}
}
.atto_image_button_text-top {
vertical-align: text-top;
}
.atto_image_button_middle {
vertical-align: middle;
}
.atto_image_button_text-bottom {
vertical-align: text-bottom;
}
.atto_image_button_left {
@include float(start);
@include margin(0, 0.5em, 0, 0);
}
.atto_image_button_right {
@include float(end);
@include margin(0, 0, 0, 0.5em);
}

View File

@ -0,0 +1,34 @@
.alert {
position: relative;
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border: 0 solid transparent;
border-radius: .5rem;
}
// Headings for larger alerts
.alert-heading {
// Specified to prevent conflicts of changing headings-color
color: inherit;
}
// Provide class for links that match alerts
.alert-link {
font-weight: 400;
}
@each $color-name, $unused in global.$colors {
.alert-#{$color-name} {
--color-base: var(--#{$color-name});
color: var(--#{$color-name}-shade);
border-color: var(--color-base);
background-color: var(--#{$color-name}-tint);
.alert-link, a {
color: var(--#{$color-name}-shade);
}
}
.alert-#{$color-name} p {
color: var(--color-base);
}
}

View File

@ -0,0 +1,54 @@
// Base class
//
// Requires one of the contextual, color modifier classes for `color` and
// `background-color`.
.badge {
display: inline-block;
padding: $badge-padding-y $badge-padding-x;
@include font-size($badge-font-size);
font-weight: $badge-font-weight;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
@include border-radius($badge-border-radius);
@include transition($badge-transition);
@at-root a#{&} {
@include hover-focus() {
text-decoration: none;
}
}
// Empty badges collapse automatically
&:empty {
display: none;
}
}
// Quick fix for badges in buttons
.btn .badge {
position: relative;
top: -1px;
}
// Pill badges
//
// Make them extra rounded with a modifier to replace v3's badges.
.badge-pill {
padding-right: $badge-pill-padding-x;
padding-left: $badge-pill-padding-x;
@include border-radius($badge-pill-border-radius);
}
// Colors
//
// Contextual variations (linked badges get darker on :hover).
@each $color, $value in $theme-colors {
.badge-#{$color} {
@include badge-variant($value);
}
}

View File

@ -0,0 +1,23 @@
.btn-link {
background: none;
}
button, .btn {
margin: 4px 8px;
padding-left: 12px;
padding-right: 12px;
border-radius: var(--core-input-radius);
a {
color: inherit;
}
}
@each $color-name, $unused in global.$colors {
.btn.btn-#{$color-name} {
--color-base: var(--#{$color-name});
color: var(--#{$color-name}-shade);
border-color: var(--color-base);
background-color: var(--#{$color-name}-tint);
}
}

View File

@ -0,0 +1,286 @@
//
// Base styles
//
.card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106
height: $card-height;
word-wrap: break-word;
background-color: $card-bg;
background-clip: border-box;
border: $card-border-width solid $card-border-color;
@include border-radius($card-border-radius);
> hr {
margin-right: 0;
margin-left: 0;
}
> .list-group {
border-top: inherit;
border-bottom: inherit;
&:first-child {
border-top-width: 0;
@include border-top-radius($card-inner-border-radius);
}
&:last-child {
border-bottom-width: 0;
@include border-bottom-radius($card-inner-border-radius);
}
}
// Due to specificity of the above selector (`.card > .list-group`), we must
// use a child selector here to prevent double borders.
> .card-header + .list-group,
> .list-group + .card-footer {
border-top: 0;
}
}
.card-body {
// Enable `flex-grow: 1` for decks and groups so that card blocks take up
// as much space as possible, ensuring footers are aligned to the bottom.
flex: 1 1 auto;
// Workaround for the image size bug in IE
// See: https://github.com/twbs/bootstrap/pull/28855
min-height: 1px;
padding: $card-spacer-x;
color: $card-color;
}
.card-title {
margin-bottom: $card-spacer-y;
}
.card-subtitle {
margin-top: -$card-spacer-y * .5;
margin-bottom: 0;
}
.card-text:last-child {
margin-bottom: 0;
}
.card-link {
@include hover() {
text-decoration: none;
}
+ .card-link {
margin-left: $card-spacer-x;
}
}
//
// Optional textual caps
//
.card-header {
padding: $card-spacer-y $card-spacer-x;
margin-bottom: 0; // Removes the default margin-bottom of <hN>
color: $card-cap-color;
background-color: $card-cap-bg;
border-bottom: $card-border-width solid $card-border-color;
&:first-child {
@include border-radius($card-inner-border-radius $card-inner-border-radius 0 0);
}
}
.card-footer {
padding: $card-spacer-y $card-spacer-x;
color: $card-cap-color;
background-color: $card-cap-bg;
border-top: $card-border-width solid $card-border-color;
&:last-child {
@include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius);
}
}
//
// Header navs
//
.card-header-tabs {
margin-right: -$card-spacer-x * .5;
margin-bottom: -$card-spacer-y;
margin-left: -$card-spacer-x * .5;
border-bottom: 0;
}
.card-header-pills {
margin-right: -$card-spacer-x * .5;
margin-left: -$card-spacer-x * .5;
}
// Card image
.card-img-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: $card-img-overlay-padding;
@include border-radius($card-inner-border-radius);
}
.card-img,
.card-img-top,
.card-img-bottom {
flex-shrink: 0; // For IE: https://github.com/twbs/bootstrap/issues/29396
width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch
}
.card-img,
.card-img-top {
@include border-top-radius($card-inner-border-radius);
}
.card-img,
.card-img-bottom {
@include border-bottom-radius($card-inner-border-radius);
}
// Card deck
.card-deck {
.card {
margin-bottom: $card-deck-margin;
}
@include media-breakpoint-up(sm) {
display: flex;
flex-flow: row wrap;
margin-right: -$card-deck-margin;
margin-left: -$card-deck-margin;
.card {
// Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4
flex: 1 0 0%;
margin-right: $card-deck-margin;
margin-bottom: 0; // Override the default
margin-left: $card-deck-margin;
}
}
}
//
// Card groups
//
.card-group {
// The child selector allows nested `.card` within `.card-group`
// to display properly.
> .card {
margin-bottom: $card-group-margin;
}
@include media-breakpoint-up(sm) {
display: flex;
flex-flow: row wrap;
// The child selector allows nested `.card` within `.card-group`
// to display properly.
> .card {
// Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4
flex: 1 0 0%;
margin-bottom: 0;
+ .card {
margin-left: 0;
border-left: 0;
}
// Handle rounded corners
@if $enable-rounded {
&:not(:last-child) {
@include border-right-radius(0);
.card-img-top,
.card-header {
// stylelint-disable-next-line property-disallowed-list
border-top-right-radius: 0;
}
.card-img-bottom,
.card-footer {
// stylelint-disable-next-line property-disallowed-list
border-bottom-right-radius: 0;
}
}
&:not(:first-child) {
@include border-left-radius(0);
.card-img-top,
.card-header {
// stylelint-disable-next-line property-disallowed-list
border-top-left-radius: 0;
}
.card-img-bottom,
.card-footer {
// stylelint-disable-next-line property-disallowed-list
border-bottom-left-radius: 0;
}
}
}
}
}
}
//
// Columns
//
.card-columns {
.card {
margin-bottom: $card-columns-margin;
}
@include media-breakpoint-up(sm) {
column-count: $card-columns-count;
column-gap: $card-columns-gap;
orphans: 1;
widows: 1;
.card {
display: inline-block; // Don't let them vertically span multiple columns
width: 100%; // Don't let their width change
}
}
}
//
// Accordion
//
.accordion {
overflow-anchor: none;
> .card {
overflow: hidden;
&:not(:last-of-type) {
border-bottom: 0;
@include border-bottom-radius(0);
}
&:not(:first-of-type) {
@include border-top-radius(0);
}
> .card-header {
@include border-radius(0);
margin-bottom: -$card-border-width;
}
}
}

View File

@ -0,0 +1,36 @@
.form-check {
position: relative;
display: block;
padding-left: 1.25rem;
}
.form-check-input {
position: absolute;
margin-top: .3rem;
margin-left: -1.25rem;
&[disabled] ~ .form-check-label,
&:disabled ~ .form-check-label {
color: global.$gray-600;
}
}
.form-check-label {
margin-bottom: 0;
}
.form-check-inline {
display: inline-flex;
align-items: center;
padding-left: 0;
margin-right: .75rem;
.form-check-input {
position: static;
margin-top: 0;
margin-right: .3125rem;
margin-left: 0;
}
}

View File

@ -0,0 +1,190 @@
// Bootstrap functions
//
// Utility mixins and functions for evaluating source code across our variables, maps, and mixins.
// Ascending
// Used to evaluate Sass maps like our grid breakpoints.
@mixin _assert-ascending($map, $map-name) {
$prev-key: null;
$prev-num: null;
@each $key, $num in $map {
@if $prev-num == null or unit($num) == "%" or unit($prev-num) == "%" {
// Do nothing
} @else if not comparable($prev-num, $num) {
@warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
} @else if $prev-num >= $num {
@warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
}
$prev-key: $key;
$prev-num: $num;
}
}
// Starts at zero
// Used to ensure the min-width of the lowest breakpoint starts at 0.
@mixin _assert-starts-at-zero($map, $map-name: "$grid-breakpoints") {
@if length($map) > 0 {
$values: map-values($map);
$first-value: nth($values, 1);
@if $first-value != 0 {
@warn "First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}.";
}
}
}
// Replace `$search` with `$replace` in `$string`
// Used on our SVG icon backgrounds for custom forms.
//
// @author Hugo Giraudel
// @param {String} $string - Initial string
// @param {String} $search - Substring to replace
// @param {String} $replace ('') - New value
// @return {String} - Updated string
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
// See https://codepen.io/kevinweber/pen/dXWoRw
//
// Requires the use of quotes around data URIs.
@function escape-svg($string) {
@if str-index($string, "data:image/svg+xml") {
@each $char, $encoded in $escaped-characters {
// Do not escape the url brackets
@if str-index($string, "url(") == 1 {
$string: url("#{str-replace(str-slice($string, 6, -3), $char, $encoded)}");
} @else {
$string: str-replace($string, $char, $encoded);
}
}
}
@return $string;
}
// Color contrast
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
$r: red($color);
$g: green($color);
$b: blue($color);
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) * .001;
@if ($yiq >= $yiq-contrasted-threshold) {
@return $dark;
} @else {
@return $light;
}
}
// Retrieve color Sass maps
@function color($key: "blue") {
@return map-get($colors, $key);
}
@function theme-color($key: "primary") {
@return map-get($theme-colors, $key);
}
@function gray($key: "100") {
@return map-get($grays, $key);
}
// Request a theme color level
@function theme-color-level($color-name: "primary", $level: 0) {
$color: theme-color($color-name);
$color-base: if($level > 0, $black, $white);
$level: abs($level);
@return mix($color-base, $color, $level * $theme-color-interval);
}
// Return valid calc
@function add($value1, $value2, $return-calc: true) {
@if $value1 == null {
@return $value2;
}
@if $value2 == null {
@return $value1;
}
@if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {
@return $value1 + $value2;
}
@return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(" + ") + $value2);
}
@function subtract($value1, $value2, $return-calc: true) {
@if $value1 == null and $value2 == null {
@return null;
}
@if $value1 == null {
@return -$value2;
}
@if $value2 == null {
@return $value1;
}
@if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {
@return $value1 - $value2;
}
@if type-of($value2) != number {
$value2: unquote("(") + $value2 + unquote(")");
}
@return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(" - ") + $value2);
}
@function divide($dividend, $divisor, $precision: 10) {
$sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);
$dividend: abs($dividend);
$divisor: abs($divisor);
@if $dividend == 0 {
@return 0;
}
@if $divisor == 0 {
@error "Cannot divide by 0";
}
$remainder: $dividend;
$result: 0;
$factor: 10;
@while ($remainder > 0 and $precision >= 0) {
$quotient: 0;
@while ($remainder >= $divisor) {
$remainder: $remainder - $divisor;
$quotient: $quotient + 1;
}
$result: $result * 10 + $quotient;
$factor: $factor * .1;
$remainder: $remainder * 10;
$precision: $precision - 1;
@if ($precision < 0 and $remainder >= $divisor * 5) {
$result: $result + 1;
}
}
$result: $result * $factor * $sign;
$dividend-unit: unit($dividend);
$divisor-unit: unit($divisor);
$unit-map: (
"px": 1px,
"rem": 1rem,
"em": 1em,
"%": 1%
);
@if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {
$result: $result * map-get($unit-map, $dividend-unit);
}
@return $result;
}

View File

@ -0,0 +1,73 @@
// Container widths
//
// Set the container width, and override it for fixed navbars in media queries.
@if $enable-grid-classes {
// Single container class with breakpoint max-widths
.container,
// 100% wide container at all breakpoints
.container-fluid {
@include make-container();
}
// Responsive containers that are 100% wide until a breakpoint
@each $breakpoint, $container-max-width in $container-max-widths {
.container-#{$breakpoint} {
@extend .container-fluid;
}
@include media-breakpoint-up($breakpoint, $grid-breakpoints) {
%responsive-container-#{$breakpoint} {
max-width: $container-max-width;
}
// Extend each breakpoint which is smaller or equal to the current breakpoint
$extend-breakpoint: true;
@each $name, $width in $grid-breakpoints {
@if ($extend-breakpoint) {
.container#{breakpoint-infix($name, $grid-breakpoints)} {
@extend %responsive-container-#{$breakpoint};
}
// Once the current breakpoint is reached, stop extending
@if ($breakpoint == $name) {
$extend-breakpoint: false;
}
}
}
}
}
}
// Row
//
// Rows contain your columns.
@if $enable-grid-classes {
.row {
@include make-row();
}
// Remove the negative margin from default .row, then the horizontal padding
// from all immediate children columns (to prevent runaway style inheritance).
.no-gutters {
margin-right: 0;
margin-left: 0;
> .col,
> [class*="col-"] {
padding-right: 0;
padding-left: 0;
}
}
}
// Columns
//
// Common styles for small and large grid columns
@if $enable-grid-classes {
@include make-grid-columns();
}

View File

@ -0,0 +1,8 @@
.media {
display: flex;
align-items: flex-start;
}
.media-body {
flex: 1;
}

View File

@ -0,0 +1,47 @@
// Toggles
//
// Used in conjunction with global variables to enable certain theme features.
// Vendor
@import "vendor/rfs";
// Deprecate
@import "mixins/deprecate";
// Utilities
@import "mixins/breakpoints";
@import "mixins/hover";
@import "mixins/image";
@import "mixins/badge";
@import "mixins/resize";
@import "mixins/screen-reader";
@import "mixins/size";
@import "mixins/reset-text";
@import "mixins/text-emphasis";
@import "mixins/text-hide";
@import "mixins/text-truncate";
@import "mixins/visibility";
// Components
@import "mixins/alert";
@import "mixins/buttons";
@import "mixins/caret";
@import "mixins/pagination";
@import "mixins/lists";
@import "mixins/list-group";
@import "mixins/nav-divider";
@import "mixins/forms";
@import "mixins/table-row";
// Skins
@import "mixins/background-variant";
@import "mixins/border-radius";
@import "mixins/box-shadow";
@import "mixins/gradients";
@import "mixins/transition";
// Layout
@import "mixins/clearfix";
@import "mixins/grid-framework";
@import "mixins/grid";
@import "mixins/float";

View File

@ -0,0 +1,6 @@
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}

View File

@ -0,0 +1,5 @@
@import "utilities/align";
@import "utilities/borders";
@import "utilities/flex";
@import "utilities/text";
@import "utilities/visibility";

View File

@ -0,0 +1,217 @@
// Variables
//
// Variables should follow the `$component-state-property-size` formula for
// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.
// Color system
$white: global.$white;
$gray-100: global.$gray-100;
$gray-200: global.$gray-200;
$gray-300: global.$gray-300;
$gray-400: global.$gray-400;
$gray-500: global.$gray-500;
$gray-600: global.$gray-600;
$gray-700: global.$gray-700;
$gray-800: global.$gray-800;
$gray-900: global.$gray-900;
$black: global.$black;
$grays: (
"100": $gray-100,
"200": $gray-200,
"300": $gray-300,
"400": $gray-400,
"500": $gray-500,
"600": $gray-600,
"700": $gray-700,
"800": $gray-800,
"900": $gray-900
);
$primary: global.$primary;
$secondary: global.$secondary;
$success: global.$success;
$info: global.$info;
$warning: global.$warning;
$danger: global.$danger;
$light: global.$light;
$dark: global.$dark;
$theme-colors: (
"primary": $primary,
"secondary": $secondary,
"success": $success,
"info": $info,
"warning": $warning,
"danger": $danger,
"light": $light,
"dark": $dark
);
// Set a specific jump point for requesting color jumps
$theme-color-interval: 8%;
// The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255.
$yiq-contrasted-threshold: 150;
// Customize the light and dark text colors for use in our YIQ color contrast function.
$yiq-text-dark: $gray-900;
$yiq-text-light: $white;
// Characters which are escaped by the escape-svg function
$escaped-characters: (
("<", "%3c"),
(">", "%3e"),
("#", "%23"),
("(", "%28"),
(")", "%29"),
);
// Options
//
// Quickly modify global styling by enabling or disabling optional features.
$enable-caret: true;
$enable-rounded: true;
$enable-shadows: false;
$enable-gradients: false;
$enable-transitions: true;
$enable-prefers-reduced-motion-media-query: true;
$enable-hover-media-query: false; // Deprecated, no longer affects any compiled CSS
$enable-grid-classes: true;
$enable-pointer-cursor-for-buttons: true;
$enable-print-styles: true;
$enable-responsive-font-sizes: false;
$enable-validation-icons: true;
$enable-deprecation-messages: true;
// Spacing
//
// Control the default styling of most Bootstrap elements by modifying these
// variables. Mostly focused on spacing.
// You can add more entries to the $spacers map, should you need more variation.
$spacer: 1rem;
$spacers: (
0: 0,
1: ($spacer * .25),
2: ($spacer * .5),
3: $spacer,
4: ($spacer * 1.5),
5: ($spacer * 3)
);
// This variable affects the `.h-*` and `.w-*` classes.
$sizes: (
25: 25%,
50: 50%,
75: 75%,
100: 100%,
auto: auto
);
// Grid breakpoints
//
// Define the minimum dimensions at which your layout will change,
// adapting to different screen sizes, for use in media queries.
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px
);
@include _assert-ascending($grid-breakpoints, "$grid-breakpoints");
@include _assert-starts-at-zero($grid-breakpoints, "$grid-breakpoints");
// Grid containers
//
// Define the maximum width of `.container` for different screen sizes.
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 960px,
xl: 1140px
);
@include _assert-ascending($container-max-widths, "$container-max-widths");
// Grid columns
//
// Set the number of columns and specify the width of the gutters.
$grid-columns: 12;
$grid-gutter-width: 30px;
$grid-row-columns: 6;
// Typography
//
// Font, line-height, and color for body text, headings, and more.
$font-weight-lighter: lighter !default;
$font-weight-light: 300 !default;
$font-weight-normal: 400 !default;
$font-weight-bold: 700 !default;
$font-weight-bolder: bolder !default;
// Cards
$card-spacer-y: .75rem;
$card-spacer-x: 1.25rem;
$card-border-width: var(--ion-card-border-width);
$card-border-radius: var(--ion-card-radius);
$card-border-color: var(--ion-card-border-color);
$card-inner-border-radius: subtract($card-border-radius, $card-border-width);
$card-cap-bg: rgba($black, .03);
$card-cap-color: null;
$card-height: null;
$card-color: null;
$card-bg: $white;
$card-img-overlay-padding: 1.25rem;
$card-group-margin: $grid-gutter-width * .5;
$card-deck-margin: $card-group-margin;
$card-columns-count: 3;
$card-columns-gap: 1.25rem;
$card-columns-margin: $card-spacer-y;
// Buttons + Forms
//
// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.
$input-btn-focus-width: .2rem !default;
// Buttons
//
// For each of Bootstrap's buttons, define text, background, and border color.
$btn-focus-width: $input-btn-focus-width !default;
$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
// Badges
$badge-font-size: 75%;
$badge-font-weight: $font-weight-bold;
$badge-padding-y: .25em;
$badge-padding-x: .4em;
$badge-border-radius: var(--mdl-shape-borderRadius-lg);
$badge-transition: $btn-transition;
$badge-focus-width: $input-btn-focus-width;
$badge-pill-padding-x: .6em;
// Use a higher than normal value to ensure completely rounded edges when
// customizing padding or font-size on labels.
$badge-pill-border-radius: 10rem;

View File

@ -0,0 +1,39 @@
@use "theme/globals" as global;
// Bootstrap 4 Styles
// -------------------------
@import "reboot";
@import "forms";
@import "buttons";
@import "alert";
@import "media";
@import "utilities";
// Deprecated styles.
.label {
display: inline-block;
padding: .25em .4em;
font-size: 75%;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
color: var(--white);
background-color: var(--medium);
}
.label-important {
color: var(--danger-contrast);
background-color: var(--danger);
}
@each $color-name, $unused in global.$colors {
.label-#{$color-name} {
color: var(--#{$color-name}-contrast);
background-color: var(--#{$color-name});
}
}

View File

@ -0,0 +1,17 @@
@use "theme/globals" as global;
// Bootstrap 4 only to be applied on database (to be removed on 4.5 and included in the main bootstrap file).
// -------------------------
// @TODO on 4.5 Move the following lines to bootstrap file.
@import "functions";
@import "variables";
@import "mixins";
@import "grid";
@import "card";
@import "badge";
// @TODO on 4.5 Move the to utilities file.
@import "utilities/spacing";
@import "bootstrap";

View File

@ -0,0 +1,13 @@
@mixin alert-variant($background, $border, $color) {
color: $color;
@include gradient-bg($background);
border-color: $border;
hr {
border-top-color: darken($border, 5%);
}
.alert-link {
color: darken($color, 10%);
}
}

View File

@ -0,0 +1,23 @@
// stylelint-disable declaration-no-important
// Contextual backgrounds
@mixin bg-variant($parent, $color, $ignore-warning: false) {
#{$parent} {
background-color: $color !important;
}
a#{$parent},
button#{$parent} {
@include hover-focus() {
background-color: darken($color, 10%) !important;
}
}
@include deprecate("The `bg-variant` mixin", "v4.4.0", "v5", $ignore-warning);
}
@mixin bg-gradient-variant($parent, $color, $ignore-warning: false) {
#{$parent} {
background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x !important;
}
@include deprecate("The `bg-gradient-variant` mixin", "v4.5.0", "v5", $ignore-warning);
}

View File

@ -0,0 +1,17 @@
@mixin badge-variant($bg) {
color: color-yiq($bg);
background-color: $bg;
@at-root a#{&} {
@include hover-focus() {
color: color-yiq($bg);
background-color: darken($bg, 10%);
}
&:focus,
&.focus {
outline: 0;
box-shadow: 0 0 0 $badge-focus-width rgba($bg, .5);
}
}
}

View File

@ -0,0 +1,76 @@
// stylelint-disable property-disallowed-list
// Single side border-radius
// Helper function to replace negative values with 0
@function valid-radius($radius) {
$return: ();
@each $value in $radius {
@if type-of($value) == number {
$return: append($return, max($value, 0));
} @else {
$return: append($return, $value);
}
}
@return $return;
}
@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {
@if $enable-rounded {
border-radius: valid-radius($radius);
}
@else if $fallback-border-radius != false {
border-radius: $fallback-border-radius;
}
}
@mixin border-top-radius($radius) {
@if $enable-rounded {
border-top-left-radius: valid-radius($radius);
border-top-right-radius: valid-radius($radius);
}
}
@mixin border-right-radius($radius) {
@if $enable-rounded {
border-top-right-radius: valid-radius($radius);
border-bottom-right-radius: valid-radius($radius);
}
}
@mixin border-bottom-radius($radius) {
@if $enable-rounded {
border-bottom-right-radius: valid-radius($radius);
border-bottom-left-radius: valid-radius($radius);
}
}
@mixin border-left-radius($radius) {
@if $enable-rounded {
border-top-left-radius: valid-radius($radius);
border-bottom-left-radius: valid-radius($radius);
}
}
@mixin border-top-left-radius($radius) {
@if $enable-rounded {
border-top-left-radius: valid-radius($radius);
}
}
@mixin border-top-right-radius($radius) {
@if $enable-rounded {
border-top-right-radius: valid-radius($radius);
}
}
@mixin border-bottom-right-radius($radius) {
@if $enable-rounded {
border-bottom-right-radius: valid-radius($radius);
}
}
@mixin border-bottom-left-radius($radius) {
@if $enable-rounded {
border-bottom-left-radius: valid-radius($radius);
}
}

View File

@ -0,0 +1,20 @@
@mixin box-shadow($shadow...) {
@if $enable-shadows {
$result: ();
@if (length($shadow) == 1) {
// We can pass `@include box-shadow(none);`
$result: $shadow;
} @else {
// Filter to avoid invalid properties for example `box-shadow: none, 1px 1px black;`
@for $i from 1 through length($shadow) {
@if nth($shadow, $i) != "none" {
$result: append($result, nth($shadow, $i), "comma");
}
}
}
@if (length($result) > 0) {
box-shadow: $result;
}
}
}

View File

@ -0,0 +1,123 @@
// Breakpoint viewport sizes and media queries.
//
// Breakpoints are defined as a map of (name: minimum width), order from small to large:
//
// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)
//
// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.
// Name of the next breakpoint, or null for the last breakpoint.
//
// >> breakpoint-next(sm)
// md
// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// md
// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))
// md
@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
$n: index($breakpoint-names, $name);
@return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);
}
// Minimum breakpoint width. Null for the smallest (first) breakpoint.
//
// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// 576px
@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
$min: map-get($breakpoints, $name);
@return if($min != 0, $min, null);
}
// Maximum breakpoint width. Null for the largest (last) breakpoint.
// The maximum value is calculated as the minimum of the next one less 0.02px
// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.
// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
// See https://bugs.webkit.org/show_bug.cgi?id=178261
//
// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// 767.98px
@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
$next: breakpoint-next($name, $breakpoints);
@return if($next, breakpoint-min($next, $breakpoints) - .02, null);
}
// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.
// Useful for making responsive utilities.
//
// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// "" (Returns a blank string)
// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// "-sm"
@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {
@return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}");
}
// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
// Makes the @content apply to the given breakpoint and wider.
@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($name, $breakpoints);
@if $min {
@media (min-width: $min) {
@content;
}
} @else {
@content;
}
}
// Media of at most the maximum breakpoint width. No query for the largest breakpoint.
// Makes the @content apply to the given breakpoint and narrower.
@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
$max: breakpoint-max($name, $breakpoints);
@if $max {
@media (max-width: $max) {
@content;
}
} @else {
@content;
}
}
// Media that spans multiple breakpoint widths.
// Makes the @content apply between the min and max breakpoints
@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($lower, $breakpoints);
$max: breakpoint-max($upper, $breakpoints);
@if $min != null and $max != null {
@media (min-width: $min) and (max-width: $max) {
@content;
}
} @else if $max == null {
@include media-breakpoint-up($lower, $breakpoints) {
@content;
}
} @else if $min == null {
@include media-breakpoint-down($upper, $breakpoints) {
@content;
}
}
}
// Media between the breakpoint's minimum and maximum widths.
// No minimum for the smallest breakpoint, and no maximum for the largest one.
// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.
@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($name, $breakpoints);
$max: breakpoint-max($name, $breakpoints);
@if $min != null and $max != null {
@media (min-width: $min) and (max-width: $max) {
@content;
}
} @else if $max == null {
@include media-breakpoint-up($name, $breakpoints) {
@content;
}
} @else if $min == null {
@include media-breakpoint-down($name, $breakpoints) {
@content;
}
}
}

View File

@ -0,0 +1,110 @@
// Button variants
//
// Easily pump out default styles, as well as :hover, :focus, :active,
// and disabled options for all buttons
@mixin button-variant($background, $border, $hover-background: darken($background, 7.5%), $hover-border: darken($border, 10%), $active-background: darken($background, 10%), $active-border: darken($border, 12.5%)) {
color: color-yiq($background);
@include gradient-bg($background);
border-color: $border;
@include box-shadow($btn-box-shadow);
@include hover() {
color: color-yiq($hover-background);
@include gradient-bg($hover-background);
border-color: $hover-border;
}
&:focus,
&.focus {
color: color-yiq($hover-background);
@include gradient-bg($hover-background);
border-color: $hover-border;
@if $enable-shadows {
@include box-shadow($btn-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));
} @else {
// Avoid using mixin so we can pass custom focus shadow properly
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
}
}
// Disabled comes first so active can properly restyle
&.disabled,
&:disabled {
color: color-yiq($background);
background-color: $background;
border-color: $border;
// Remove CSS gradients if they're enabled
@if $enable-gradients {
background-image: none;
}
}
&:not(:disabled):not(.disabled):active,
&:not(:disabled):not(.disabled).active,
.show > &.dropdown-toggle {
color: color-yiq($active-background);
background-color: $active-background;
@if $enable-gradients {
background-image: none; // Remove the gradient for the pressed/active state
}
border-color: $active-border;
&:focus {
@if $enable-shadows and $btn-active-box-shadow != none {
@include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));
} @else {
// Avoid using mixin so we can pass custom focus shadow properly
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
}
}
}
}
@mixin button-outline-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {
color: $color;
border-color: $color;
@include hover() {
color: $color-hover;
background-color: $active-background;
border-color: $active-border;
}
&:focus,
&.focus {
box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);
}
&.disabled,
&:disabled {
color: $color;
background-color: transparent;
}
&:not(:disabled):not(.disabled):active,
&:not(:disabled):not(.disabled).active,
.show > &.dropdown-toggle {
color: color-yiq($active-background);
background-color: $active-background;
border-color: $active-border;
&:focus {
@if $enable-shadows and $btn-active-box-shadow != none {
@include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5));
} @else {
// Avoid using mixin so we can pass custom focus shadow properly
box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);
}
}
}
}
// Button sizes
@mixin button-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {
padding: $padding-y $padding-x;
@include font-size($font-size);
line-height: $line-height;
// Manually declare to provide an override to the browser default
@include border-radius($border-radius, 0);
}

View File

@ -0,0 +1,62 @@
@mixin caret-down() {
border-top: $caret-width solid;
border-right: $caret-width solid transparent;
border-bottom: 0;
border-left: $caret-width solid transparent;
}
@mixin caret-up() {
border-top: 0;
border-right: $caret-width solid transparent;
border-bottom: $caret-width solid;
border-left: $caret-width solid transparent;
}
@mixin caret-right() {
border-top: $caret-width solid transparent;
border-right: 0;
border-bottom: $caret-width solid transparent;
border-left: $caret-width solid;
}
@mixin caret-left() {
border-top: $caret-width solid transparent;
border-right: $caret-width solid;
border-bottom: $caret-width solid transparent;
}
@mixin caret($direction: down) {
@if $enable-caret {
&::after {
display: inline-block;
margin-left: $caret-spacing;
vertical-align: $caret-vertical-align;
content: "";
@if $direction == down {
@include caret-down();
} @else if $direction == up {
@include caret-up();
} @else if $direction == right {
@include caret-right();
}
}
@if $direction == left {
&::after {
display: none;
}
&::before {
display: inline-block;
margin-right: $caret-spacing;
vertical-align: $caret-vertical-align;
content: "";
@include caret-left();
}
}
&:empty::after {
margin-left: 0;
}
}
}

View File

@ -0,0 +1,7 @@
@mixin clearfix() {
&::after {
display: block;
clear: both;
content: "";
}
}

View File

@ -0,0 +1,10 @@
// Deprecate mixin
//
// This mixin can be used to deprecate mixins or functions.
// `$enable-deprecation-messages` is a global variable, `$ignore-warning` is a variable that can be passed to
// some deprecated mixins to suppress the warning (for example if the mixin is still be used in the current version of Bootstrap)
@mixin deprecate($name, $deprecate-version, $remove-version, $ignore-warning: false) {
@if ($enable-deprecation-messages != false and $ignore-warning != true) {
@warn "#{$name} has been deprecated as of #{$deprecate-version}. It will be removed entirely in #{$remove-version}.";
}
}

View File

@ -0,0 +1,14 @@
// stylelint-disable declaration-no-important
@mixin float-left() {
float: left !important;
@include deprecate("The `float-left` mixin", "v4.3.0", "v5");
}
@mixin float-right() {
float: right !important;
@include deprecate("The `float-right` mixin", "v4.3.0", "v5");
}
@mixin float-none() {
float: none !important;
@include deprecate("The `float-none` mixin", "v4.3.0", "v5");
}

View File

@ -0,0 +1,195 @@
// Form control focus state
//
// Generate a customized focus state and for any input with the specified color,
// which defaults to the `$input-focus-border-color` variable.
//
// We highly encourage you to not customize the default value, but instead use
// this to tweak colors on an as-needed basis. This aesthetic change is based on
// WebKit's default styles, but applicable to a wider range of browsers. Its
// usability and accessibility should be taken into account with any change.
//
// Example usage: change the default blue border and shadow to white for better
// contrast against a dark gray background.
@mixin form-control-focus($ignore-warning: false) {
&:focus {
color: $input-focus-color;
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
outline: 0;
@if $enable-shadows {
@include box-shadow($input-box-shadow, $input-focus-box-shadow);
} @else {
// Avoid using mixin so we can pass custom focus shadow properly
box-shadow: $input-focus-box-shadow;
}
}
@include deprecate("The `form-control-focus()` mixin", "v4.4.0", "v5", $ignore-warning);
}
// This mixin uses an `if()` technique to be compatible with Dart Sass
// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details
@mixin form-validation-state-selector($state) {
@if ($state == "valid" or $state == "invalid") {
.was-validated #{if(&, "&", "")}:#{$state},
#{if(&, "&", "")}.is-#{$state} {
@content;
}
} @else {
#{if(&, "&", "")}.is-#{$state} {
@content;
}
}
}
@mixin form-validation-state($state, $color, $icon) {
.#{$state}-feedback {
display: none;
width: 100%;
margin-top: $form-feedback-margin-top;
@include font-size($form-feedback-font-size);
color: $color;
}
.#{$state}-tooltip {
position: absolute;
top: 100%;
left: 0;
z-index: 5;
display: none;
max-width: 100%; // Contain to parent when possible
padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x;
margin-top: .1rem;
@include font-size($form-feedback-tooltip-font-size);
line-height: $form-feedback-tooltip-line-height;
color: color-yiq($color);
background-color: rgba($color, $form-feedback-tooltip-opacity);
@include border-radius($form-feedback-tooltip-border-radius);
// See https://github.com/twbs/bootstrap/pull/31557
// Align tooltip to form elements
.form-row > .col > &,
.form-row > [class*="col-"] > & {
left: $form-grid-gutter-width * .5;
}
}
@include form-validation-state-selector($state) {
~ .#{$state}-feedback,
~ .#{$state}-tooltip {
display: block;
}
}
.form-control {
@include form-validation-state-selector($state) {
border-color: $color;
@if $enable-validation-icons {
padding-right: $input-height-inner !important; // stylelint-disable-line declaration-no-important
background-image: escape-svg($icon);
background-repeat: no-repeat;
background-position: right $input-height-inner-quarter center;
background-size: $input-height-inner-half $input-height-inner-half;
}
&:focus {
border-color: $color;
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
}
}
}
// stylelint-disable-next-line selector-no-qualifying-type
select.form-control {
@include form-validation-state-selector($state) {
@if $enable-validation-icons {
padding-right: $input-padding-x * 4 !important; // stylelint-disable-line declaration-no-important
background-position: right $input-padding-x * 2 center;
}
}
}
// stylelint-disable-next-line selector-no-qualifying-type
textarea.form-control {
@include form-validation-state-selector($state) {
@if $enable-validation-icons {
padding-right: $input-height-inner;
background-position: top $input-height-inner-quarter right $input-height-inner-quarter;
}
}
}
.custom-select {
@include form-validation-state-selector($state) {
border-color: $color;
@if $enable-validation-icons {
padding-right: $custom-select-feedback-icon-padding-right !important; // stylelint-disable-line declaration-no-important
background: $custom-select-background, $custom-select-bg escape-svg($icon) $custom-select-feedback-icon-position / $custom-select-feedback-icon-size no-repeat;
}
&:focus {
border-color: $color;
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
}
}
}
.form-check-input {
@include form-validation-state-selector($state) {
~ .form-check-label {
color: $color;
}
~ .#{$state}-feedback,
~ .#{$state}-tooltip {
display: block;
}
}
}
.custom-control-input {
@include form-validation-state-selector($state) {
~ .custom-control-label {
color: $color;
&::before {
border-color: $color;
}
}
&:checked {
~ .custom-control-label::before {
border-color: lighten($color, 10%);
@include gradient-bg(lighten($color, 10%));
}
}
&:focus {
~ .custom-control-label::before {
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
}
&:not(:checked) ~ .custom-control-label::before {
border-color: $color;
}
}
}
}
// custom file
.custom-file-input {
@include form-validation-state-selector($state) {
~ .custom-file-label {
border-color: $color;
}
&:focus {
~ .custom-file-label {
border-color: $color;
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
}
}
}
}
}

View File

@ -0,0 +1,45 @@
// Gradients
@mixin gradient-bg($color) {
@if $enable-gradients {
background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;
} @else {
background-color: $color;
}
}
// Horizontal gradient, from left to right
//
// Creates two color stops, start and end, by specifying a color and position for each color stop.
@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {
background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);
background-repeat: repeat-x;
}
// Vertical gradient, from top to bottom
//
// Creates two color stops, start and end, by specifying a color and position for each color stop.
@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {
background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);
background-repeat: repeat-x;
}
@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) {
background-image: linear-gradient($deg, $start-color, $end-color);
background-repeat: repeat-x;
}
@mixin gradient-x-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {
background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);
background-repeat: no-repeat;
}
@mixin gradient-y-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {
background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);
background-repeat: no-repeat;
}
@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) {
background-image: radial-gradient(circle, $inner-color, $outer-color);
background-repeat: no-repeat;
}
@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) {
background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
}

View File

@ -0,0 +1,69 @@
/// Grid system
//
// Generate semantic grid columns with these mixins.
@mixin make-container($gutter: $grid-gutter-width) {
width: 100%;
padding-right: $gutter * .5;
padding-left: $gutter * .5;
margin-right: auto;
margin-left: auto;
}
@mixin make-row($gutter: $grid-gutter-width) {
display: flex;
flex-wrap: wrap;
margin-right: -$gutter * .5;
margin-left: -$gutter * .5;
}
// For each breakpoint, define the maximum width of the container in a media query
@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {
@each $breakpoint, $container-max-width in $max-widths {
@include media-breakpoint-up($breakpoint, $breakpoints) {
max-width: $container-max-width;
}
}
@include deprecate("The `make-container-max-widths` mixin", "v4.5.2", "v5");
}
@mixin make-col-ready($gutter: $grid-gutter-width) {
position: relative;
// Prevent columns from becoming too narrow when at smaller grid tiers by
// always setting `width: 100%;`. This works because we use `flex` values
// later on to override this initial width.
width: 100%;
padding-right: $gutter * .5;
padding-left: $gutter * .5;
}
@mixin make-col($size, $columns: $grid-columns) {
flex: 0 0 percentage(divide($size, $columns));
// Add a `max-width` to ensure content within each column does not blow out
// the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
// do not appear to require this.
max-width: percentage(divide($size, $columns));
}
@mixin make-col-auto() {
flex: 0 0 auto;
width: auto;
max-width: 100%; // Reset earlier grid tiers
}
@mixin make-col-offset($size, $columns: $grid-columns) {
$num: divide($size, $columns);
margin-left: if($num == 0, 0, percentage($num));
}
// Row columns
//
// Specify on a parent element(e.g., .row) to force immediate children into NN
// numberof columns. Supports wrapping to new lines, but does not do a Masonry
// style grid.
@mixin row-cols($count) {
> * {
flex: 0 0 divide(100%, $count);
max-width: divide(100%, $count);
}
}

View File

@ -0,0 +1,80 @@
// Framework grid generation
//
// Used only by Bootstrap to generate the correct number of grid classes given
// any value of `$grid-columns`.
@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {
// Common properties for all breakpoints
%grid-column {
position: relative;
width: 100%;
padding-right: $gutter * .5;
padding-left: $gutter * .5;
}
@each $breakpoint in map-keys($breakpoints) {
$infix: breakpoint-infix($breakpoint, $breakpoints);
@if $columns > 0 {
// Allow columns to stretch full width below their breakpoints
@for $i from 1 through $columns {
.col#{$infix}-#{$i} {
@extend %grid-column;
}
}
}
.col#{$infix},
.col#{$infix}-auto {
@extend %grid-column;
}
@include media-breakpoint-up($breakpoint, $breakpoints) {
// Provide basic `.col-{bp}` classes for equal-width flexbox columns
.col#{$infix} {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
}
@if $grid-row-columns > 0 {
@for $i from 1 through $grid-row-columns {
.row-cols#{$infix}-#{$i} {
@include row-cols($i);
}
}
}
.col#{$infix}-auto {
@include make-col-auto();
}
@if $columns > 0 {
@for $i from 1 through $columns {
.col#{$infix}-#{$i} {
@include make-col($i, $columns);
}
}
}
.order#{$infix}-first { order: -1; }
.order#{$infix}-last { order: $columns + 1; }
@for $i from 0 through $columns {
.order#{$infix}-#{$i} { order: $i; }
}
@if $columns > 0 {
// `$columns - 1` because offsetting by the width of an entire row isn't possible
@for $i from 0 through ($columns - 1) {
@if not ($infix == "" and $i == 0) { // Avoid emitting useless .offset-0
.offset#{$infix}-#{$i} {
@include make-col-offset($i, $columns);
}
}
}
}
}
}
}

View File

@ -0,0 +1,69 @@
/// Grid system
//
// Generate semantic grid columns with these mixins.
@mixin make-container($gutter: $grid-gutter-width) {
width: 100%;
padding-right: $gutter * .5;
padding-left: $gutter * .5;
margin-right: auto;
margin-left: auto;
}
@mixin make-row($gutter: $grid-gutter-width) {
display: flex;
flex-wrap: wrap;
margin-right: -$gutter * .5;
margin-left: -$gutter * .5;
}
// For each breakpoint, define the maximum width of the container in a media query
@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {
@each $breakpoint, $container-max-width in $max-widths {
@include media-breakpoint-up($breakpoint, $breakpoints) {
max-width: $container-max-width;
}
}
@include deprecate("The `make-container-max-widths` mixin", "v4.5.2", "v5");
}
@mixin make-col-ready($gutter: $grid-gutter-width) {
position: relative;
// Prevent columns from becoming too narrow when at smaller grid tiers by
// always setting `width: 100%;`. This works because we use `flex` values
// later on to override this initial width.
width: 100%;
padding-right: $gutter * .5;
padding-left: $gutter * .5;
}
@mixin make-col($size, $columns: $grid-columns) {
flex: 0 0 percentage(divide($size, $columns));
// Add a `max-width` to ensure content within each column does not blow out
// the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
// do not appear to require this.
max-width: percentage(divide($size, $columns));
}
@mixin make-col-auto() {
flex: 0 0 auto;
width: auto;
max-width: 100%; // Reset earlier grid tiers
}
@mixin make-col-offset($size, $columns: $grid-columns) {
$num: divide($size, $columns);
margin-left: if($num == 0, 0, percentage($num));
}
// Row columns
//
// Specify on a parent element(e.g., .row) to force immediate children into NN
// numberof columns. Supports wrapping to new lines, but does not do a Masonry
// style grid.
@mixin row-cols($count) {
> * {
flex: 0 0 divide(100%, $count);
max-width: divide(100%, $count);
}
}

View File

@ -0,0 +1,37 @@
// Hover mixin and `$enable-hover-media-query` are deprecated.
//
// Originally added during our alphas and maintained during betas, this mixin was
// designed to prevent `:hover` stickiness on iOS-an issue where hover styles
// would persist after initial touch.
//
// For backward compatibility, we've kept these mixins and updated them to
// always return their regular pseudo-classes instead of a shimmed media query.
//
// Issue: https://github.com/twbs/bootstrap/issues/25195
@mixin hover() {
&:hover { @content; }
}
@mixin hover-focus() {
&:hover,
&:focus {
@content;
}
}
@mixin plain-hover-focus() {
&,
&:hover,
&:focus {
@content;
}
}
@mixin hover-focus-active() {
&:hover,
&:focus,
&:active {
@content;
}
}

View File

@ -0,0 +1,36 @@
// Image Mixins
// - Responsive image
// - Retina image
// Responsive image
//
// Keep images from scaling beyond the width of their parents.
@mixin img-fluid() {
// Part 1: Set a maximum relative to the parent
max-width: 100%;
// Part 2: Override the height to auto, otherwise images will be stretched
// when setting a width and height attribute on the img element.
height: auto;
}
// Retina image
//
// Short retina mixin for setting background-image and -size.
@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {
background-image: url($file-1x);
// Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,
// but doesn't convert dppx=>dpi.
// There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.
// Compatibility info: https://caniuse.com/css-media-resolution
@media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx
only screen and (min-resolution: 2dppx) { // Standardized
background-image: url($file-2x);
background-size: $width-1x $height-1x;
}
@include deprecate("`img-retina()`", "v4.3.0", "v5");
}

View File

@ -0,0 +1,21 @@
// List Groups
@mixin list-group-item-variant($state, $background, $color) {
.list-group-item-#{$state} {
color: $color;
background-color: $background;
&.list-group-item-action {
@include hover-focus() {
color: $color;
background-color: darken($background, 5%);
}
&.active {
color: $white;
background-color: $color;
border-color: $color;
}
}
}
}

View File

@ -0,0 +1,7 @@
// Lists
// Unstyled keeps list items block level, just removes default browser padding and list-style
@mixin list-unstyled() {
padding-left: 0;
list-style: none;
}

View File

@ -0,0 +1,11 @@
// Horizontal dividers
//
// Dividers (basically an hr) within dropdowns and nav lists
@mixin nav-divider($color: $nav-divider-color, $margin-y: $nav-divider-margin-y, $ignore-warning: false) {
height: 0;
margin: $margin-y 0;
overflow: hidden;
border-top: 1px solid $color;
@include deprecate("The `nav-divider()` mixin", "v4.4.0", "v5", $ignore-warning);
}

View File

@ -0,0 +1,22 @@
// Pagination
@mixin pagination-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {
.page-link {
padding: $padding-y $padding-x;
@include font-size($font-size);
line-height: $line-height;
}
.page-item {
&:first-child {
.page-link {
@include border-left-radius($border-radius);
}
}
&:last-child {
.page-link {
@include border-right-radius($border-radius);
}
}
}
}

View File

@ -0,0 +1,17 @@
@mixin reset-text() {
font-family: $font-family-base;
// We deliberately do NOT reset font-size or word-wrap.
font-style: normal;
font-weight: $font-weight-normal;
line-height: $line-height-base;
text-align: left; // Fallback for where `start` is not supported
text-align: start;
text-decoration: none;
text-shadow: none;
text-transform: none;
letter-spacing: normal;
word-break: normal;
white-space: normal;
word-spacing: normal;
line-break: auto;
}

View File

@ -0,0 +1,6 @@
// Resize anything
@mixin resizable($direction) {
overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible`
resize: $direction; // Options: horizontal, vertical, both
}

View File

@ -0,0 +1,34 @@
// Only display content to screen readers
//
// See: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
// See: https://kittygiraudel.com/2016/10/13/css-hide-and-seek/
@mixin sr-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px; // Fix for https://github.com/twbs/bootstrap/issues/25686
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
@mixin sr-only-focusable() {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
overflow: visible;
clip: auto;
white-space: normal;
}
}

View File

@ -0,0 +1,7 @@
// Sizing shortcuts
@mixin size($width, $height: $width) {
width: $width;
height: $height;
@include deprecate("`size()`", "v4.3.0", "v5");
}

View File

@ -0,0 +1,39 @@
// Tables
@mixin table-row-variant($state, $background, $border: null) {
// Exact selectors below required to override `.table-striped` and prevent
// inheritance to nested tables.
.table-#{$state} {
&,
> th,
> td {
background-color: $background;
}
@if $border != null {
th,
td,
thead th,
tbody + tbody {
border-color: $border;
}
}
}
// Hover states for `.table-hover`
// Note: this is not available for cells or rows within `thead` or `tfoot`.
.table-hover {
$hover-background: darken($background, 5%);
.table-#{$state} {
@include hover() {
background-color: $hover-background;
> td,
> th {
background-color: $hover-background;
}
}
}
}
}

View File

@ -0,0 +1,17 @@
// stylelint-disable declaration-no-important
// Typography
@mixin text-emphasis-variant($parent, $color, $ignore-warning: false) {
#{$parent} {
color: $color !important;
}
@if $emphasized-link-hover-darken-percentage != 0 {
a#{$parent} {
@include hover-focus() {
color: darken($color, $emphasized-link-hover-darken-percentage) !important;
}
}
}
@include deprecate("`text-emphasis-variant()`", "v4.4.0", "v5", $ignore-warning);
}

View File

@ -0,0 +1,11 @@
// CSS image replacement
@mixin text-hide($ignore-warning: false) {
// stylelint-disable-next-line font-family-no-missing-generic-family-keyword
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
@include deprecate("`text-hide()`", "v4.1.0", "v5", $ignore-warning);
}

View File

@ -0,0 +1,8 @@
// Text truncate
// Requires inline-block or block for proper styling
@mixin text-truncate() {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -0,0 +1,26 @@
// stylelint-disable property-disallowed-list
@mixin transition($transition...) {
@if length($transition) == 0 {
$transition: $transition-base;
}
@if length($transition) > 1 {
@each $value in $transition {
@if $value == null or $value == none {
@warn "The keyword 'none' or 'null' must be used as a single argument.";
}
}
}
@if $enable-transitions {
@if nth($transition, 1) != null {
transition: $transition;
}
@if $enable-prefers-reduced-motion-media-query and nth($transition, 1) != null and nth($transition, 1) != none {
@media (prefers-reduced-motion: reduce) {
transition: none;
}
}
}
}

View File

@ -0,0 +1,8 @@
// stylelint-disable declaration-no-important
// Visibility
@mixin invisible($visibility) {
visibility: $visibility !important;
@include deprecate("`invisible()`", "v4.3.0", "v5");
}

View File

@ -0,0 +1,8 @@
// stylelint-disable declaration-no-important
.align-baseline { vertical-align: baseline !important; } // Browser default
.align-top { vertical-align: top !important; }
.align-middle { vertical-align: middle !important; }
.align-bottom { vertical-align: bottom !important; }
.align-text-bottom { vertical-align: text-bottom !important; }
.align-text-top { vertical-align: text-top !important; }

View File

@ -0,0 +1,69 @@
.border { border: 1px solid var(--gray-500) !important; }
.border-top { border-top: 1px solid var(--gray-500) !important; }
.border-right { border-right: 1px solid var(--gray-500) !important; }
.border-bottom { border-bottom: 1px solid var(--gray-500) !important; }
.border-left { border-left: 1px solid var(--gray-500) !important; }
.border-0 { border: 0 !important; }
.border-top-0 { border-top: 0 !important; }
.border-right-0 { border-right: 0 !important; }
.border-bottom-0 { border-bottom: 0 !important; }
.border-left-0 { border-left: 0 !important; }
@each $color-name, $unused in global.$colors {
.border-#{$color-name} {
border-color: var(--#{$color-name}) !important;
}
}
.border-white {
border-color: var(--white) !important;
}
//
// Border-radius
//
.rounded-sm {
border-radius: var(--mdl-shape-borderRadius-sm) !important;
}
.rounded {
border-radius: var(--mdl-shape-borderRadius-md) !important;
}
.rounded-top {
border-top-left-radius: var(--mdl-shape-borderRadius-md) !important;
border-top-right-radius: var(--mdl-shape-borderRadius-md) !important;
}
.rounded-right {
border-top-right-radius: var(--mdl-shape-borderRadius-md) !important;
border-bottom-right-radius: var(--mdl-shape-borderRadius-md) !important;
}
.rounded-bottom {
border-bottom-right-radius: var(--mdl-shape-borderRadius-md) !important;
border-bottom-left-radius: var(--mdl-shape-borderRadius-md) !important;
}
.rounded-left {
border-top-left-radius: var(--mdl-shape-borderRadius-md) !important;
border-bottom-left-radius: var(--mdl-shape-borderRadius-md) !important;
}
.rounded-lg {
border-radius: var(--mdl-shape-borderRadius-lg) !important;
}
.rounded-circle {
border-radius: 50% !important;
}
.rounded-pill {
border-radius: 50rem !important;
}
.rounded-0 {
border-radius: 0 !important;
}

View File

@ -0,0 +1,39 @@
.flex-row { flex-direction: row !important; }
.flex-column { flex-direction: column !important; }
.flex-row-reverse { flex-direction: row-reverse !important; }
.flex-column-reverse { flex-direction: column-reverse !important; }
.flex-wrap { flex-wrap: wrap !important; }
.flex-nowrap { flex-wrap: nowrap !important; }
.flex-wrap-reverse { flex-wrap: wrap-reverse !important; }
.flex-fill { flex: 1 1 auto !important; }
.flex-grow-0 { flex-grow: 0 !important; }
.flex-grow-1 { flex-grow: 1 !important; }
.flex-shrink-0 { flex-shrink: 0 !important; }
.flex-shrink-1 { flex-shrink: 1 !important; }
.justify-content-start { justify-content: flex-start !important; }
.justify-content-end { justify-content: flex-end !important; }
.justify-content-center { justify-content: center !important; }
.justify-content-between { justify-content: space-between !important; }
.justify-content-around { justify-content: space-around !important; }
.align-items-start { align-items: flex-start !important; }
.align-items-end { align-items: flex-end !important; }
.align-items-center { align-items: center !important; }
.align-items-baseline { align-items: baseline !important; }
.align-items-stretch { align-items: stretch !important; }
.align-content-start { align-content: flex-start !important; }
.align-content-end { align-content: flex-end !important; }
.align-content-center { align-content: center !important; }
.align-content-between { align-content: space-between !important; }
.align-content-around { align-content: space-around !important; }
.align-content-stretch { align-content: stretch !important; }
.align-self-auto { align-self: auto !important; }
.align-self-start { align-self: flex-start !important; }
.align-self-end { align-self: flex-end !important; }
.align-self-center { align-self: center !important; }
.align-self-baseline { align-self: baseline !important; }
.align-self-stretch { align-self: stretch !important; }

View File

@ -1,4 +1,7 @@
// Text for accessibility, hidden from the view. //
// Screenreaders
//
.sr-only, .accesshide { .sr-only, .accesshide {
@include sr-only(); @include sr-only();

View File

@ -0,0 +1,73 @@
// stylelint-disable declaration-no-important
// Margin and Padding
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
@each $prop, $abbrev in (margin: m, padding: p) {
@each $size, $length in $spacers {
.#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }
.#{$abbrev}t#{$infix}-#{$size},
.#{$abbrev}y#{$infix}-#{$size} {
#{$prop}-top: $length !important;
}
.#{$abbrev}r#{$infix}-#{$size},
.#{$abbrev}x#{$infix}-#{$size} {
#{$prop}-right: $length !important;
}
.#{$abbrev}b#{$infix}-#{$size},
.#{$abbrev}y#{$infix}-#{$size} {
#{$prop}-bottom: $length !important;
}
.#{$abbrev}l#{$infix}-#{$size},
.#{$abbrev}x#{$infix}-#{$size} {
#{$prop}-left: $length !important;
}
}
}
// Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)
@each $size, $length in $spacers {
@if "#{$size}" != "0" {
.m#{$infix}-n#{$size} { margin: -$length !important; }
.mt#{$infix}-n#{$size},
.my#{$infix}-n#{$size} {
margin-top: -$length !important;
}
.mr#{$infix}-n#{$size},
.mx#{$infix}-n#{$size} {
margin-right: -$length !important;
}
.mb#{$infix}-n#{$size},
.my#{$infix}-n#{$size} {
margin-bottom: -$length !important;
}
.ml#{$infix}-n#{$size},
.mx#{$infix}-n#{$size} {
margin-left: -$length !important;
}
}
}
// Some special margin utils
.m#{$infix}-auto { margin: auto !important; }
.mt#{$infix}-auto,
.my#{$infix}-auto {
margin-top: auto !important;
}
.mr#{$infix}-auto,
.mx#{$infix}-auto {
margin-right: auto !important;
}
.mb#{$infix}-auto,
.my#{$infix}-auto {
margin-bottom: auto !important;
}
.ml#{$infix}-auto,
.mx#{$infix}-auto {
margin-left: auto !important;
}
}
}

View File

@ -0,0 +1,64 @@
// stylelint-disable declaration-no-important
//
// Text
//
.text-monospace { font-family: var(--mdl-typography-monospace-font) !important; }
// Alignment
.text-justify { text-align: justify !important; }
.text-wrap { white-space: normal !important; }
.text-nowrap { white-space: nowrap !important; }
.text-truncate { @include global.ellipsis(); }
// Responsive alignment
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
.text-center { text-align: center !important; }
// Transformation
.text-lowercase { text-transform: lowercase !important; }
.text-uppercase { text-transform: uppercase !important; }
.text-capitalize { text-transform: capitalize !important; }
// Weight and italics
.font-weight-light { font-weight: 300 !important; }
.font-weight-lighter { font-weight: lighter !important; }
.font-weight-normal { font-weight: 400 !important; }
.font-weight-bold { font-weight: 700 !important; }
.font-weight-bolder { font-weight: bolder !important; }
.font-italic { font-style: italic !important; }
// Contextual colors
.text-white { color: var(--white) !important; }
@each $color-name, $unused in global.$colors {
.text-#{$color-name} {
color: var(--#{$color-name});
}
}
.text-body { color: var(--ion-text-color) !important; }
.text-muted { color: var(--subdued-text-color) !important; }
.text-black-50 { color: rgb(0 0 0 / 50%) !important; }
.text-white-50 { color: rgb(255 255 255 / 50%) !important; }
// Misc
.text-decoration-none { text-decoration: none !important; }
.text-break {
word-break: break-word !important; // Deprecated, but avoids issues with flex containers
word-wrap: break-word !important; // Used instead of `overflow-wrap` for IE & Edge Legacy
}
// Reset
.text-reset { color: inherit !important; }

View File

@ -0,0 +1,13 @@
// stylelint-disable declaration-no-important
//
// Visibility utilities
//
.visible {
visibility: visible !important;
}
.invisible {
visibility: hidden !important;
}

View File

@ -0,0 +1,228 @@
// stylelint-disable property-blacklist, scss/dollar-variable-default
// SCSS RFS mixin
//
// Automated responsive font sizes
//
// Licensed under MIT (https://github.com/twbs/rfs/blob/v8.x/LICENSE)
// Configuration
// Base font size
$rfs-base-font-size: 1.25rem !default;
$rfs-font-size-unit: rem !default;
@if $rfs-font-size-unit != rem and $rfs-font-size-unit != px {
@error "`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.";
}
// Breakpoint at where font-size starts decreasing if screen width is smaller
$rfs-breakpoint: 1200px !default;
$rfs-breakpoint-unit: px !default;
@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {
@error "`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.";
}
// Resize font size based on screen height and width
$rfs-two-dimensional: false !default;
// Factor of decrease
$rfs-factor: 10 !default;
@if type-of($rfs-factor) != "number" or $rfs-factor <= 1 {
@error "`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.";
}
// Generate enable or disable classes. Possibilities: false, "enable" or "disable"
$rfs-class: false !default;
// 1 rem = $rfs-rem-value px
$rfs-rem-value: 16 !default;
// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14
$rfs-safari-iframe-resize-bug-fix: false !default;
// Disable RFS by setting $enable-responsive-font-sizes to false
$enable-responsive-font-sizes: true !default;
// Cache $rfs-base-font-size unit
$rfs-base-font-size-unit: unit($rfs-base-font-size);
@function divide($dividend, $divisor, $precision: 10) {
$sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);
$dividend: abs($dividend);
$divisor: abs($divisor);
@if $dividend == 0 {
@return 0;
}
@if $divisor == 0 {
@error "Cannot divide by 0";
}
$remainder: $dividend;
$result: 0;
$factor: 10;
@while ($remainder > 0 and $precision >= 0) {
$quotient: 0;
@while ($remainder >= $divisor) {
$remainder: $remainder - $divisor;
$quotient: $quotient + 1;
}
$result: $result * 10 + $quotient;
$factor: $factor * .1;
$remainder: $remainder * 10;
$precision: $precision - 1;
@if ($precision < 0 and $remainder >= $divisor * 5) {
$result: $result + 1;
}
}
$result: $result * $factor * $sign;
$dividend-unit: unit($dividend);
$divisor-unit: unit($divisor);
$unit-map: (
"px": 1px,
"rem": 1rem,
"em": 1em,
"%": 1%
);
@if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {
$result: $result * map-get($unit-map, $dividend-unit);
}
@return $result;
}
// Remove px-unit from $rfs-base-font-size for calculations
@if $rfs-base-font-size-unit == "px" {
$rfs-base-font-size: divide($rfs-base-font-size, $rfs-base-font-size * 0 + 1);
}
@else if $rfs-base-font-size-unit == "rem" {
$rfs-base-font-size: divide($rfs-base-font-size, divide($rfs-base-font-size * 0 + 1, $rfs-rem-value));
}
// Cache $rfs-breakpoint unit to prevent multiple calls
$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);
// Remove unit from $rfs-breakpoint for calculations
@if $rfs-breakpoint-unit-cache == "px" {
$rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);
}
@else if $rfs-breakpoint-unit-cache == "rem" or $rfs-breakpoint-unit-cache == "em" {
$rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));
}
// Internal mixin that adds disable classes to the selector if needed.
@mixin _rfs-disable-class {
@if $rfs-class == "disable" {
// Adding an extra class increases specificity, which prevents the media query to override the font size
&,
.disable-responsive-font-size &,
&.disable-responsive-font-size {
@content;
}
}
@else {
@content;
}
}
// Internal mixin that adds enable classes to the selector if needed.
@mixin _rfs-enable-class {
@if $rfs-class == "enable" {
.enable-responsive-font-size &,
&.enable-responsive-font-size {
@content;
}
}
@else {
@content;
}
}
// Internal mixin used to determine which media query needs to be used
@mixin _rfs-media-query($mq-value) {
@if $rfs-two-dimensional {
@media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {
@content;
}
}
@else {
@media (max-width: #{$mq-value}) {
@content;
}
}
}
// Responsive font size mixin
@mixin rfs($fs, $important: false) {
// Cache $fs unit
$fs-unit: if(type-of($fs) == "number", unit($fs), false);
// Add !important suffix if needed
$rfs-suffix: if($important, " !important", "");
// If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value
@if not $fs-unit or $fs-unit != "" and $fs-unit != "px" and $fs-unit != "rem" or $fs == 0 {
font-size: #{$fs}#{$rfs-suffix};
}
@else {
// Remove unit from $fs for calculations
@if $fs-unit == "px" {
$fs: divide($fs, $fs * 0 + 1);
}
@else if $fs-unit == "rem" {
$fs: divide($fs, divide($fs * 0 + 1, $rfs-rem-value));
}
// Set default font size
$rfs-static: if($rfs-font-size-unit == rem, #{divide($fs, $rfs-rem-value)}rem, #{$fs}px);
// Only add the media query if the font size is bigger than the minimum font size
@if $fs <= $rfs-base-font-size or not $enable-responsive-font-sizes {
font-size: #{$rfs-static}#{$rfs-suffix};
}
@else {
// Calculate the minimum font size for $fs
$fs-min: $rfs-base-font-size + divide($fs - $rfs-base-font-size, $rfs-factor);
// Calculate difference between $fs and the minimum font size
$fs-diff: $fs - $fs-min;
// Base font-size formatting
$min-width: if($rfs-font-size-unit == rem, #{divide($fs-min, $rfs-rem-value)}rem, #{$fs-min}px);
// Use `vmin` if two-dimensional is enabled
$variable-unit: if($rfs-two-dimensional, vmin, vw);
// Calculate the variable width between 0 and $rfs-breakpoint
$variable-width: #{divide($fs-diff * 100, $rfs-breakpoint)}#{$variable-unit};
// Set the calculated font-size
$rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};
// Breakpoint formatting
$mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});
@include _rfs-disable-class {
font-size: #{$rfs-static}#{$rfs-suffix};
}
@include _rfs-media-query($mq-value) {
@include _rfs-enable-class {
font-size: $rfs-fluid;
}
// Include safari iframe resize fix if needed
min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);
}
}
}
}
// The font-size & responsive-font-size mixins use RFS to rescale the font size
@mixin font-size($fs, $important: false) {
@include rfs($fs, $important);
}
@mixin responsive-font-size($fs, $important: false) {
@include rfs($fs, $important);
}

View File

@ -364,343 +364,10 @@ core-rich-text-editor .core-rte-editor {
flex-shrink: 0; flex-shrink: 0;
} }
// Atto styles @import "theme/components/atto.scss";
// ------------------------- // @TODO on 4.5 Remove the comment below.
.atto_image_preview { // @import "theme/components/moodle.scss";
width: 100%; @import "theme/components/bootstrap/bootstrap.scss";
height: 100%;
margin-left: auto;
margin-right: auto;
}
.atto_image_preview_box {
max-height: 200px;
margin-bottom: 1em;
overflow: auto;
}
.editor_atto_content img {
cursor: pointer;
}
.atto_image_size {
display: inline-block;
}
.atto_image_size input[type=checkbox] {
@include margin(null, 1em, null, 1em);
}
.atto_image_size input[type=text] {
width: 3em;
}
.atto_image_size label {
display: inline-block;
}
.atto_image_button_text-top,
.atto_image_button_middle,
.atto_image_button_text-bottom,
.atto_image_button_left,
.atto_image_button_right {
vertical-align: middle;
max-width: 100%;
display: inline-block;
margin: 0 0.5em;
&.img-responsive {
/* If the image is display: block then linking the image to URLs won't work. */
/*display: inline-block;*/
max-width: 100%;
}
}
.atto_image_button_text-top {
vertical-align: text-top;
}
.atto_image_button_middle {
vertical-align: middle;
}
.atto_image_button_text-bottom {
vertical-align: text-bottom;
}
.atto_image_button_left {
@include float(start);
@include margin(0, 0.5em, 0, 0);
}
.atto_image_button_right {
@include float(end);
@include margin(0, 0, 0, 0.5em);
}
// Bootstrap 4 Styles
// -------------------------
// _reboot.scss
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
// _media.scss
.media {
display: flex;
align-items: flex-start;
}
.media-body {
flex: 1;
}
// _alert.scss
.alert {
position: relative;
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border: 0 solid transparent;
border-radius: .5rem;
}
// Headings for larger alerts
.alert-heading {
// Specified to prevent conflicts of changing headings-color
color: inherit;
}
// Provide class for links that match alerts
.alert-link {
font-weight: 400;
}
@each $color-name, $unused in global.$colors {
.alert-#{$color-name} {
--color-base: var(--#{$color-name});
color: var(--#{$color-name}-shade);
border-color: var(--color-base);
background-color: var(--#{$color-name}-tint);
.alert-link, a {
color: var(--#{$color-name}-shade);
}
}
.alert-#{$color-name} p {
color: var(--color-base);
}
}
// _forms.scss
.form-check {
position: relative;
display: block;
padding-left: 1.25rem;
}
.form-check-input {
position: absolute;
margin-top: .3rem;
margin-left: -1.25rem;
&[disabled] ~ .form-check-label,
&:disabled ~ .form-check-label {
color: global.$gray-600;
}
}
.form-check-label {
margin-bottom: 0;
}
.form-check-inline {
display: inline-flex;
align-items: center;
padding-left: 0;
margin-right: .75rem;
.form-check-input {
position: static;
margin-top: 0;
margin-right: .3125rem;
margin-left: 0;
}
}
// utilities/_align.scss
.align-baseline { vertical-align: baseline !important; } // Browser default
.align-top { vertical-align: top !important; }
.align-middle { vertical-align: middle !important; }
.align-bottom { vertical-align: bottom !important; }
.align-text-bottom { vertical-align: text-bottom !important; }
.align-text-top { vertical-align: text-top !important; }
// utilities/_border.scss
.border { border: 1px solid var(--gray-500) !important; }
.border-top { border-top: 1px solid var(--gray-500) !important; }
.border-right { border-right: 1px solid var(--gray-500) !important; }
.border-bottom { border-bottom: 1px solid var(--gray-500) !important; }
.border-left { border-left: 1px solid var(--gray-500) !important; }
.border-0 { border: 0 !important; }
.border-top-0 { border-top: 0 !important; }
.border-right-0 { border-right: 0 !important; }
.border-bottom-0 { border-bottom: 0 !important; }
.border-left-0 { border-left: 0 !important; }
@each $color-name, $unused in global.$colors {
.border-#{$color-name} {
border-color: var(--#{$color-name}) !important;
}
}
.border-white {
border-color: var(--white) !important;
}
// utilities/_flex.scss
.flex-row { flex-direction: row !important; }
.flex-column { flex-direction: column !important; }
.flex-row-reverse { flex-direction: row-reverse !important; }
.flex-column-reverse { flex-direction: column-reverse !important; }
.flex-wrap { flex-wrap: wrap !important; }
.flex-nowrap { flex-wrap: nowrap !important; }
.flex-wrap-reverse { flex-wrap: wrap-reverse !important; }
.flex-fill { flex: 1 1 auto !important; }
.flex-grow-0 { flex-grow: 0 !important; }
.flex-grow-1 { flex-grow: 1 !important; }
.flex-shrink-0 { flex-shrink: 0 !important; }
.flex-shrink-1 { flex-shrink: 1 !important; }
.justify-content-start { justify-content: flex-start !important; }
.justify-content-end { justify-content: flex-end !important; }
.justify-content-center { justify-content: center !important; }
.justify-content-between { justify-content: space-between !important; }
.justify-content-around { justify-content: space-around !important; }
.align-items-start { align-items: flex-start !important; }
.align-items-end { align-items: flex-end !important; }
.align-items-center { align-items: center !important; }
.align-items-baseline { align-items: baseline !important; }
.align-items-stretch { align-items: stretch !important; }
.align-content-start { align-content: flex-start !important; }
.align-content-end { align-content: flex-end !important; }
.align-content-center { align-content: center !important; }
.align-content-between { align-content: space-between !important; }
.align-content-around { align-content: space-around !important; }
.align-content-stretch { align-content: stretch !important; }
.align-self-auto { align-self: auto !important; }
.align-self-start { align-self: flex-start !important; }
.align-self-end { align-self: flex-end !important; }
.align-self-center { align-self: center !important; }
.align-self-baseline { align-self: baseline !important; }
.align-self-stretch { align-self: stretch !important; }
// utilities/_visibility.scss
.visible {
visibility: visible !important;
}
.invisible {
visibility: hidden !important;
}
// utilities/_text.scss
.text-monospace { font-family: var(--mdl-typography-monospace-font) !important; }
.text-justify { text-align: justify !important; }
.text-wrap { white-space: normal !important; }
.text-nowrap { white-space: nowrap !important; }
.text-truncate {
@include ellipsis();
}
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
.text-center { text-align: center !important; }
.text-lowercase { text-transform: lowercase !important; }
.text-uppercase { text-transform: uppercase !important; }
.text-capitalize { text-transform: capitalize !important; }
.font-weight-light { font-weight: 300 !important; }
.font-weight-lighter { font-weight: lighter !important; }
.font-weight-normal { font-weight: 400 !important; }
.font-weight-bold { font-weight: 700 !important; }
.font-weight-bolder { font-weight: bolder !important; }
.font-italic { font-style: italic !important; }
.text-white { color: var(--white) !important; }
@each $color-name, $unused in global.$colors {
.text-#{$color-name} {
color: var(--#{$color-name});
}
}
.text-body { color: var(--ion-text-color) !important; }
.text-muted { color: var(--subdued-text-color) !important; }
.text-black-50 { color: rgb(0 0 0 / 50%) !important; }
.text-white-50 { color: rgb(255 255 255 / 50%) !important; }
.text-decoration-none { text-decoration: none !important; }
.text-break {
word-break: break-word !important; // Deprecated, but avoids issues with flex containers
word-wrap: break-word !important; // Used instead of `overflow-wrap` for IE & Edge Legacy
}
.text-reset { color: inherit !important; }
.label {
display: inline-block;
padding: .25em .4em;
font-size: 75%;
font-weight: 700;
line-height: 1.1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
color: var(--white);
background-color: var(--medium);
}
.label-important {
color: var(--danger-contrast);
background-color: var(--danger);
}
@each $color-name, $unused in global.$colors {
.label-#{$color-name} {
color: var(--#{$color-name}-contrast);
background-color: var(--#{$color-name});
}
}
.btn-link {
background: none;
}
button, .btn {
margin: 4px 8px;
padding-left: 12px;
padding-right: 12px;
border-radius: var(--core-input-radius);
a {
color: inherit;
}
}
@each $color-name, $unused in global.$colors {
.btn.btn-#{$color-name} {
--color-base: var(--#{$color-name});
color: var(--#{$color-name}-shade);
border-color: var(--color-base);
background-color: var(--#{$color-name}-tint);
}
}
} }
// h1 is too big and ugly, reduce size when loading. // h1 is too big and ugly, reduce size when loading.

View File

@ -0,0 +1,3 @@
.d-flex {
display: flex !important;
}

View File

@ -63,7 +63,6 @@ html[dir=rtl] {
flex-direction: row; flex-direction: row;
} }
.flex-column { .flex-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -82,7 +81,6 @@ html[dir=rtl] {
.font-lg { font-size: 1.7rem; } .font-lg { font-size: 1.7rem; }
.font-sm { font-size: 1.2rem; } .font-sm { font-size: 1.2rem; }
@each $color-name, $unused in $colors { @each $color-name, $unused in $colors {
.text-#{$color-name}, .text-#{$color-name},
p.text-#{$color-name} { p.text-#{$color-name} {

View File

@ -61,7 +61,7 @@ html {
} }
/* Some styles from 3rd party libraries. */ /* Some styles from 3rd party libraries. */
@import "bootstrap.scss"; @import "components/bootstrap/utilities/screenreaders.scss";
/* Core CSS required for Ionic components to work properly */ /* Core CSS required for Ionic components to work properly */
@import "@ionic/angular/css/core.css"; @import "@ionic/angular/css/core.css";