Merge pull request #1880 from dpalou/MOBILE-2963

Mobile 2963
main
Juan Leyva 2019-05-06 11:00:56 +02:00 committed by GitHub
commit 8f8457d6ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 161 additions and 87 deletions

View File

@ -177,6 +177,7 @@
"addon.messages.contactname": "local_moodlemobileapp",
"addon.messages.contactrequestsent": "message",
"addon.messages.contacts": "message",
"addon.messages.conversationactions": "message",
"addon.messages.decline": "message",
"addon.messages.deleteallconfirm": "message",
"addon.messages.deleteallselfconfirm": "message",
@ -198,6 +199,7 @@
"addon.messages.messagepreferences": "message",
"addon.messages.messages": "message",
"addon.messages.muteconversation": "message",
"addon.messages.mutedconversation": "message",
"addon.messages.newmessage": "message",
"addon.messages.newmessages": "local_moodlemobileapp",
"addon.messages.nocontactrequests": "message",
@ -216,9 +218,6 @@
"addon.messages.requests": "moodle",
"addon.messages.requirecontacttomessage": "message",
"addon.messages.searchcombined": "message",
"addon.messages.searchnocontactsfound": "message",
"addon.messages.searchnomessagesfound": "message",
"addon.messages.searchnononcontactsfound": "message",
"addon.messages.selfconversation": "message",
"addon.messages.selfconversationdefaultmessage": "message",
"addon.messages.sendcontactrequest": "message",

View File

@ -16,6 +16,7 @@
"contactname": "Contact name",
"contactrequestsent": "Contact request sent",
"contacts": "Contacts",
"conversationactions": "Conversation actions menu",
"decline": "Decline",
"deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.",
"deleteallselfconfirm": "Are you sure you would like to delete this entire personal conversation?",
@ -37,6 +38,7 @@
"messagepreferences": "Message preferences",
"messages": "Messages",
"muteconversation": "Mute",
"mutedconversation": "Muted conversation",
"newmessage": "New message",
"newmessages": "New messages",
"nocontactrequests": "No contact requests",
@ -55,9 +57,6 @@
"requests": "Requests",
"requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.",
"searchcombined": "Search people and messages",
"searchnocontactsfound": "No contacts found",
"searchnomessagesfound": "No messages found",
"searchnononcontactsfound": "No non contacts found",
"selfconversation": "Personal space",
"selfconversationdefaultmessage": "Save draft messages, links, notes etc. to access later.",
"sendcontactrequest": "Send contact request",

View File

@ -1,16 +1,16 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>
<img *ngIf="loaded && !otherMember && conversationImage" class="core-bar-button-image" [src]="conversationImage" alt="" onError="this.src='assets/img/group-avatar.png'" core-external-content role="presentation" [siteId]="siteId || null">
<img *ngIf="loaded && !otherMember && conversationImage" class="core-bar-button-image" [src]="conversationImage" [alt]="title" onError="this.src='assets/img/group-avatar.png'" core-external-content role="presentation" [siteId]="siteId || null">
<ion-avatar *ngIf="loaded && otherMember" class="core-bar-button-image" core-user-avatar [user]="otherMember" [linkProfile]="false" [checkOnline]="otherMember.showonlinestatus" item-start (click)="showInfo && viewInfo()"></ion-avatar>
<core-format-text [text]="title" (click)="showInfo && !isGroup && viewInfo()"></core-format-text>
<core-icon *ngIf="conversation && conversation.isfavourite" name="fa-star"></core-icon>
<core-icon *ngIf="conversation && conversation.ismuted" name="volume-off"></core-icon>
<core-icon *ngIf="conversation && conversation.isfavourite" name="fa-star" [label]="'core.favourites' | translate"></core-icon>
<core-icon *ngIf="conversation && conversation.ismuted" name="volume-off" [label]="'addon.messages.mutedconversation' | translate"></core-icon>
</ion-title>
<ion-buttons end></ion-buttons>
</ion-navbar>
<core-navbar-buttons end>
<core-context-menu>
<core-context-menu [aria-label]="'addon.messages.conversationactions' | translate">
<core-context-menu-item [hidden]="isSelf || !showInfo || isGroup" [priority]="1000" [content]="'addon.messages.info' | translate" (action)="viewInfo()" iconAction="information-circle"></core-context-menu-item>
<core-context-menu-item [hidden]="isSelf || !showInfo || !isGroup" [priority]="1000" [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" iconAction="information-circle"></core-context-menu-item>
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation" [priority]="800" [content]="(conversation && conversation.isfavourite ? 'addon.messages.removefromfavourites' : 'addon.messages.addtofavourites') | translate" (action)="changeFavourite($event)" [closeOnClick]="false" [iconAction]="favouriteIcon"></core-context-menu-item>
@ -47,7 +47,7 @@
<ion-item text-wrap (longPress)="copyMessage(message)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [class.addon-message-not-mine]="message.useridfrom != currentUserId" [class.addon-message-no-user]="!message.showUserData" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<!-- User data. -->
<h2 class="addon-message-user" >
<h2 class="addon-message-user">
<ion-avatar item-start core-user-avatar [user]="members[message.useridfrom]" [linkProfile]="false" *ngIf="message.showUserData"></ion-avatar>
<div *ngIf="message.showUserData">{{ members[message.useridfrom].fullname }}</div>
@ -64,6 +64,7 @@
<button ion-button icon-only clear="true" *ngIf="!message.sending && showDelete" (click)="deleteMessage(message, index)" class="addon-messages-delete-button" [@coreSlideInOut]="'fromRight'" [attr.aria-label]=" 'addon.messages.deletemessage' | translate">
<ion-icon name="trash" color="danger"></ion-icon>
</button>
<div class="tail" *ngIf="message.showTail"></div>
</ion-item>
</ng-container>
</ion-list>

View File

@ -47,6 +47,9 @@ ion-app.app-root page-addon-messages-discussion {
min-height: 0;
position: relative;
@include core-transition(width);
// This is needed to display bubble tails.
overflow: visible;
contain: none;
core-format-text > p:only-child {
display: inline;
@ -127,6 +130,15 @@ ion-app.app-root page-addon-messages-discussion {
line-height: initial;
}
}
.tail {
content: '';
width: 0;
height: 0;
border: 0.5rem solid transparent;
position: absolute;
touch-action: none;
}
}
.addon-message.addon-message-mine + .addon-message-no-user.addon-message-mine,
@ -158,6 +170,18 @@ ion-app.app-root page-addon-messages-discussion {
height: 16px;
}
}
.tail {
@include position(null, 0, 0, null);
@include margin-horizontal(null, -0.5rem);
border-bottom-color: $item-message-mine-bg;
}
}
.addon-message-not-mine .tail {
@include position(null, null, 0, 0);
@include margin-horizontal(-0.5rem, null);
border-bottom-color: $item-message-bg;
}
.toolbar-title {

View File

@ -384,6 +384,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.messages.forEach((message, index): any => {
message.showDate = this.showDate(message, this.messages[index - 1]);
message.showUserData = this.showUserData(message, this.messages[index - 1]);
message.showTail = this.showTail(message, this.messages[index + 1]);
});
// Call resize to recalculate the dimensions.
@ -1046,6 +1047,17 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
(!prevMessage || prevMessage.useridfrom != message.useridfrom || message.showDate);
}
/**
* Check if a css tail should be shown.
*
* @param {any} message Current message where to show the user info.
* @param {any} [nextMessage] Next message.
* @return {boolean} Whether user data should be shown.
*/
showTail(message: any, nextMessage?: any): boolean {
return !nextMessage || nextMessage.useridfrom != message.useridfrom || nextMessage.showDate;
}
/**
* Toggles delete state.
*/

View File

@ -101,7 +101,7 @@
<h2>
<core-format-text [text]="conversation.name"></core-format-text>
<core-icon name="fa-ban" *ngIf="conversation.isblocked" [label]="'addon.messages.contactblocked' | translate"></core-icon>
<core-icon *ngIf="conversation.ismuted" name="volume-off"></core-icon>
<core-icon *ngIf="conversation.ismuted" name="volume-off" [label]="'addon.messages.mutedconversation' | translate"></core-icon>
</h2>
<ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount">
<ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge>

View File

@ -266,7 +266,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
const promises = [];
promises.push(this.fetchConversationCounts());
promises.push(this.messagesProvider.getContactRequestsCount(this.siteId)); // View updated by the event observer.
// View updated by the events observers.
promises.push(this.messagesProvider.getContactRequestsCount(this.siteId));
if (refreshUnreadCounts) {
promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
return Promise.all(promises).then(() => {
if (typeof this.favourites.expanded == 'undefined') {
@ -276,9 +281,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
// We don't know which option it belongs to, so we need to fetch the data for all of them.
const promises = [];
promises.push(this.fetchDataForOption(this.favourites, false, refreshUnreadCounts));
promises.push(this.fetchDataForOption(this.group, false, refreshUnreadCounts));
promises.push(this.fetchDataForOption(this.individual, false, refreshUnreadCounts));
promises.push(this.fetchDataForOption(this.favourites, false));
promises.push(this.fetchDataForOption(this.group, false));
promises.push(this.fetchDataForOption(this.individual, false));
return Promise.all(promises).then(() => {
// All conversations have been loaded, find the one we need to load and expand its option.
@ -286,13 +291,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
if (conversation) {
const option = this.getConversationOption(conversation);
return this.expandOption(option, refreshUnreadCounts);
return this.expandOption(option);
} else {
// Conversation not found, just open the default option.
this.calculateExpandedStatus();
// Now load the data for the expanded option.
return this.fetchDataForExpandedOption(refreshUnreadCounts);
return this.fetchDataForExpandedOption();
}
});
}
@ -302,7 +307,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
}
// Now load the data for the expanded option.
return this.fetchDataForExpandedOption(refreshUnreadCounts);
return this.fetchDataForExpandedOption();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
}).finally(() => {
@ -314,9 +319,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Calculate which option should be expanded initially.
*/
protected calculateExpandedStatus(): void {
this.favourites.expanded = this.favourites.count != 0;
this.group.expanded = this.favourites.count == 0 && this.group.count != 0;
this.individual.expanded = this.favourites.count == 0 && this.group.count == 0;
this.favourites.expanded = this.favourites.count != 0 && !this.group.unread && !this.individual.unread;
this.group.expanded = !this.favourites.expanded && this.group.count != 0 && !this.individual.unread;
this.individual.expanded = !this.favourites.expanded && !this.group.expanded;
this.loadCurrentListElement();
}
@ -324,26 +329,16 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
/**
* Fetch data for the expanded option.
*
* @param {booleam} [refreshUnreadCounts=true] Whether to refresh unread counts.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchDataForExpandedOption(refreshUnreadCounts: boolean = true): Promise<any> {
protected fetchDataForExpandedOption(): Promise<any> {
const expandedOption = this.getExpandedOption();
if (expandedOption) {
return this.fetchDataForOption(expandedOption, false, refreshUnreadCounts);
} else {
// All options are collapsed, update the counts.
const promises = [];
promises.push(this.fetchConversationCounts());
if (refreshUnreadCounts) {
// View updated by event observer.
promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
return this.fetchDataForOption(expandedOption, false);
}
return Promise.all(promises);
}
return Promise.resolve();
}
/**
@ -351,10 +346,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
*
* @param {any} option The option to fetch data for.
* @param {boolean} [loadingMore} Whether we are loading more data or just the first ones.
* @param {booleam} [refreshUnreadCounts=true] Whether to refresh unread counts.
* @param {booleam} [getCounts] Whether to get counts data.
* @return {Promise<any>} Promise resolved when done.
*/
fetchDataForOption(option: any, loadingMore?: boolean, refreshUnreadCounts: boolean = true): Promise<void> {
fetchDataForOption(option: any, loadingMore?: boolean, getCounts?: boolean): Promise<void> {
option.loadMoreError = false;
const limitFrom = loadingMore ? option.conversations.length : 0,
@ -375,12 +370,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
promises.push(this.messagesOffline.getAllMessages().then((data) => {
offlineMessages = data;
}));
promises.push(this.fetchConversationCounts());
if (refreshUnreadCounts) {
// View updated by the event observer.
promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
if (getCounts) {
promises.push(this.fetchConversationCounts());
promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
return Promise.all(promises).then(() => {
@ -650,7 +644,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
option.expanded = false;
this.loadCurrentListElement();
} else {
this.expandOption(option).catch((error) => {
// Pass getCounts=true to update the counts everytime the user expands an option.
this.expandOption(option, true).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
});
}
@ -660,10 +655,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Expand a certain option.
*
* @param {any} option The option to expand.
* @param {booleam} [refreshUnreadCounts=true] Whether to refresh unread counts.
* @param {booleam} [getCounts] Whether to get counts data.
* @return {Promise<any>} Promise resolved when done.
*/
protected expandOption(option: any, refreshUnreadCounts: boolean = true): Promise<any> {
protected expandOption(option: any, getCounts?: boolean): Promise<any> {
// Collapse all and expand the right one.
this.favourites.expanded = false;
this.group.expanded = false;
@ -672,7 +667,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
option.expanded = true;
option.loading = true;
return this.fetchDataForOption(option, false, refreshUnreadCounts).then(() => {
return this.fetchDataForOption(option, false, getCounts).then(() => {
this.loadCurrentListElement();
}).catch((error) => {
option.expanded = false;

View File

@ -18,22 +18,22 @@
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="messages.canLoadMore" (action)="search(query, 'messages', $event)" [error]="messages.loadMoreError"></core-infinite-loading>
</ion-list>
<core-empty-box *ngIf="displayResults && !contacts.results.length && !nonContacts.results.length && !messages.results.length" icon="search" [message]="'core.noresults' | translate"></core-empty-box>
</core-loading>
</ion-content>
</core-split-view>
<!-- Template to render a list of results -->
<ng-template #resultsTemplate let-item="item">
<ng-container *ngIf="item.results.length > 0">
<ion-item-divider text-wrap>{{ item.titleString | translate }}</ion-item-divider>
<ion-item text-wrap *ngIf="item.results.length == 0">
{{ item.emptyString | translate }}
</ion-item>
<!-- List of results -->
<a ion-item text-wrap *ngFor="let result of item.results" [title]="result.fullname" (click)="openConversation(result)" [class.core-split-item-selected]="result == selectedResult" class="addon-message-discussion">
<ion-avatar item-start core-user-avatar [user]="result" [checkOnline]="true" [linkProfile]="false"></ion-avatar>
<h2>
<core-format-text [text]="result.fullname"></core-format-text>
<core-format-text [text]="result.fullname" [highlight]="result.highlightName"></core-format-text>
<core-icon name="fa-ban" *ngIf="result.isblocked" [label]="'addon.messages.contactblocked' | translate"></core-icon>
</h2>
<ion-note *ngIf="result.lastmessagedate > 0">
@ -41,7 +41,7 @@
</ion-note>
<p class="addon-message-last-message">
<span *ngIf="result.sentfromcurrentuser" class="addon-message-last-message-user">{{ 'addon.messages.you' | translate }}</span>
<core-format-text clean="true" singleLine="true" [text]="result.lastmessage" class="addon-message-last-message-text"></core-format-text>
<core-format-text clean="true" singleLine="true" [text]="result.lastmessage" [highlight]="result.highlightMessage" class="addon-message-last-message-text"></core-format-text>
</p>
</a>
@ -56,4 +56,5 @@
<ion-spinner></ion-spinner>
</div>
</ng-container>
</ng-container>
</ng-template>

View File

@ -38,7 +38,6 @@ export class AddonMessagesSearchPage implements OnDestroy {
contacts = {
type: 'contacts',
titleString: 'addon.messages.contacts',
emptyString: 'addon.messages.searchnocontactsfound',
results: [],
canLoadMore: false,
loadingMore: false
@ -46,7 +45,6 @@ export class AddonMessagesSearchPage implements OnDestroy {
nonContacts = {
type: 'noncontacts',
titleString: 'addon.messages.noncontacts',
emptyString: 'addon.messages.searchnononcontactsfound',
results: [],
canLoadMore: false,
loadingMore: false
@ -54,7 +52,6 @@ export class AddonMessagesSearchPage implements OnDestroy {
messages = {
type: 'messages',
titleString: 'addon.messages.messages',
emptyString: 'addon.messages.searchnomessagesfound',
results: [],
canLoadMore: false,
loadingMore: false,
@ -178,17 +175,20 @@ export class AddonMessagesSearchPage implements OnDestroy {
if (!loadMore || loadMore == 'contacts') {
this.contacts.results.push(...newContacts);
this.contacts.canLoadMore = canLoadMoreContacts;
this.setHighlight(newContacts, true);
}
if (!loadMore || loadMore == 'noncontacts') {
this.nonContacts.results.push(...newNonContacts);
this.nonContacts.canLoadMore = canLoadMoreNonContacts;
this.setHighlight(newNonContacts, true);
}
if (!loadMore || loadMore == 'messages') {
this.messages.results.push(...newMessages);
this.messages.canLoadMore = canLoadMoreMessages;
this.messages.loadMoreError = false;
this.setHighlight(newMessages, false);
}
if (!loadMore) {
@ -241,6 +241,19 @@ export class AddonMessagesSearchPage implements OnDestroy {
}
}
/**
* Set the highlight values for each entry.
*
* @param {any[]} results Results to highlight.
* @param {boolean} isUser Whether the results are from a user search or from a message search.
*/
setHighlight(results: any[], isUser: boolean): void {
results.forEach((result) => {
result.highlightName = isUser ? this.query : undefined;
result.highlightMessage = !isUser ? this.query : undefined;
});
}
/**
* Component destroyed.
*/

View File

@ -1050,6 +1050,11 @@ ion-modal,
contain: size layout style;
}
// Highlight text.
.matchtext {
background-color: $core-text-hightlight-background-color;
}
// Styles for desktop apps only.
ion-app.platform-desktop {
video::-webkit-media-text-track-display {

View File

@ -177,6 +177,7 @@
"addon.messages.contactname": "Contact name",
"addon.messages.contactrequestsent": "Contact request sent",
"addon.messages.contacts": "Contacts",
"addon.messages.conversationactions": "Conversation actions menu",
"addon.messages.decline": "Decline",
"addon.messages.deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.",
"addon.messages.deleteallselfconfirm": "Are you sure you would like to delete this entire personal conversation?",
@ -198,6 +199,7 @@
"addon.messages.messagepreferences": "Message preferences",
"addon.messages.messages": "Messages",
"addon.messages.muteconversation": "Mute",
"addon.messages.mutedconversation": "Muted conversation",
"addon.messages.newmessage": "New message",
"addon.messages.newmessages": "New messages",
"addon.messages.nocontactrequests": "No contact requests",
@ -216,9 +218,6 @@
"addon.messages.requests": "Requests",
"addon.messages.requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.",
"addon.messages.searchcombined": "Search people and messages",
"addon.messages.searchnocontactsfound": "No contacts found",
"addon.messages.searchnomessagesfound": "No messages found",
"addon.messages.searchnononcontactsfound": "No non contacts found",
"addon.messages.selfconversation": "Personal space",
"addon.messages.selfconversationdefaultmessage": "Save draft messages, links, notes etc. to access later.",
"addon.messages.sendcontactrequest": "Send contact request",

View File

@ -31,10 +31,10 @@ import { Subject } from 'rxjs';
})
export class CoreContextMenuComponent implements OnInit, OnDestroy {
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
@Input() title?: string; // Aria label and text to be shown on the top of the popover.
@Input() title?: string; // Text to be shown on the top of the popover.
@Input('aria-label') ariaLabel?: string; // Aria label to be shown on the top of the popover.
hideMenu = true; // It will be unhidden when items are added.
ariaLabel: string;
expanded = false;
protected items: CoreContextMenuItemComponent[] = [];
protected itemsMovedToParent: CoreContextMenuItemComponent[] = [];
@ -70,7 +70,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
*/
ngOnInit(): void {
this.icon = this.icon || 'more';
this.ariaLabel = this.title || this.translate.instant('core.info');
this.ariaLabel = this.ariaLabel || this.title || this.translate.instant('core.info');
}
/**

View File

@ -1,6 +1,6 @@
<ion-list [id]="uniqueId" role="menu">
<ion-list-header *ngIf="title">{{title}}</ion-list-header>
<a ion-item text-wrap *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden" [attr.detail-none]="!item.href || item.iconAction" role="menuitem" [attr.aria-controls]="uniqueId">
<a ion-item text-wrap *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden" [attr.detail-none]="!item.href || item.iconAction" role="menuitem">
<core-icon *ngIf="item.iconDescription" [name]="item.iconDescription" [label]="item.ariaDescription" item-start></core-icon>
<core-format-text [clean]="true" [text]="item.content"></core-format-text>
<core-icon *ngIf="(item.href || item.action) && item.iconAction && item.iconAction != 'spinner'" [name]="item.iconAction" item-end></core-icon>

View File

@ -55,6 +55,7 @@ export class CoreFormatTextDirective implements OnChanges {
// If you want to avoid this use class="inline" at the same time to use display: inline-block.
@Input() fullOnClick?: boolean | string; // Whether it should open a new page with the full contents on click.
@Input() fullTitle?: string; // Title to use in full view. Defaults to "Description".
@Input() highlight?: string; // Text to highlight.
@Output() afterRender?: EventEmitter<any>; // Called when the data is rendered.
protected element: HTMLElement;
@ -348,7 +349,7 @@ export class CoreFormatTextDirective implements OnChanges {
// Apply format text function.
return this.textUtils.formatText(this.text, this.utils.isTrueOrOne(this.clean),
this.utils.isTrueOrOne(this.singleLine));
this.utils.isTrueOrOne(this.singleLine), undefined, this.highlight);
}).then((formatted) => {
const div = document.createElement('div'),
canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']);

View File

@ -396,9 +396,10 @@ export class CoreTextUtilsProvider {
* @param {boolean} [clean] Whether HTML tags should be removed.
* @param {boolean} [singleLine] Whether new lines should be removed. Only valid if clean is true.
* @param {number} [shortenLength] Number of characters to shorten the text.
* @param {number} [highlight] Text to highlight.
* @return {Promise<string>} Promise resolved with the formatted text.
*/
formatText(text: string, clean?: boolean, singleLine?: boolean, shortenLength?: number): Promise<string> {
formatText(text: string, clean?: boolean, singleLine?: boolean, shortenLength?: number, highlight?: string): Promise<string> {
return this.treatMultilangTags(text).then((formatted) => {
if (clean) {
formatted = this.cleanTags(formatted, singleLine);
@ -406,6 +407,9 @@ export class CoreTextUtilsProvider {
if (shortenLength > 0) {
formatted = this.shortenText(formatted, shortenLength);
}
if (highlight) {
formatted = this.highlightText(formatted, highlight);
}
return formatted;
});
@ -452,6 +456,25 @@ export class CoreTextUtilsProvider {
return /<[a-z][\s\S]*>/i.test(text);
}
/**
* Highlight all occurrences of a certain text inside another text. It will add some HTML code to highlight it.
*
* @param {string} text Full text.
* @param {string} searchText Text to search and highlight.
* @return {string} Highlighted text.
*/
highlightText(text: string, searchText: string): string {
if (!text || typeof text != 'string') {
return '';
} else if (!searchText) {
return text;
}
const regex = new RegExp('(' + searchText + ')', 'gi');
return text.replace(regex, '<span class="matchtext">$1</span>');
}
/**
* Check if HTML content is blank.
*

View File

@ -263,6 +263,8 @@ $core-rte-min-height: 80px;
$core-toolbar-button-image-width: 32px;
$core-text-hightlight-background-color: lighten($blue, 40%) !default;
// Timer variables.
$core-timer-warn-color: $red !default;
$core-timer-iterations: 15 !default;