Merge pull request #2134 from crazyserver/MOBILE-3205

Mobile 3205
main
Juan Leyva 2019-10-31 17:20:18 +01:00 committed by GitHub
commit 5368c32b84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 678 additions and 189 deletions

View File

@ -2,13 +2,7 @@
<ion-item text-wrap *ngIf="files && files.length && !edit">
<h2>{{plugin.name}}</h2>
<div no-lines>
<ng-container *ngFor="let file of files">
<!-- Files already attached to the submission. -->
<core-file *ngIf="!file.name" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="file.name" [file]="file"></core-local-file>
</ng-container>
<core-files [files]="files" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-files>
</div>
</ion-item>

View File

@ -10,12 +10,6 @@
<ng-container *ngIf="isShowOrListMode()">
<div no-lines>
<ng-container *ngFor="let file of files">
<!-- Files already attached to the submission. -->
<core-file *ngIf="!file.name" [file]="file" [component]="component" [componentId]="componentId" [alwaysDownload]="true"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="file.name" [file]="file"></core-local-file>
</ng-container>
<core-files [files]="files" [component]="component" [componentId]="componentId" [alwaysDownload]="true"></core-files>
</div>
</ng-container>

View File

@ -24,11 +24,13 @@ import { CoreRatingComponentsModule } from '@core/rating/components/components.m
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post';
import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
@NgModule({
declarations: [
AddonModForumIndexComponent,
AddonModForumPostComponent
AddonModForumPostComponent,
AddonForumDiscussionOptionsMenuComponent
],
imports: [
CommonModule,
@ -45,10 +47,12 @@ import { AddonModForumPostComponent } from './post/post';
],
exports: [
AddonModForumIndexComponent,
AddonModForumPostComponent
AddonModForumPostComponent,
AddonForumDiscussionOptionsMenuComponent
],
entryComponents: [
AddonModForumIndexComponent
AddonModForumIndexComponent,
AddonForumDiscussionOptionsMenuComponent
]
})
export class AddonModForumComponentsModule {}

View File

@ -0,0 +1,24 @@
<ion-item text-wrap (click)="setLockState(true)" *ngIf="discussion.canlock && !discussion.locked">
<core-icon name="fa-lock" item-start></core-icon>
<h2>{{ 'addon.mod_forum.lockdiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="setLockState(false)" *ngIf="discussion.canlock && discussion.locked">
<core-icon name="fa-unlock" item-start></core-icon>
<h2>{{ 'addon.mod_forum.unlockdiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="setPinState(true)" *ngIf="canPin && !discussion.pinned">
<core-icon name="fa-map-pin" item-start></core-icon>
<h2>{{ 'addon.mod_forum.pindiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="setPinState(false)" *ngIf="canPin && discussion.pinned">
<core-icon name="fa-map-pin" item-start [slash]="true"></core-icon>
<h2>{{ 'addon.mod_forum.unpindiscussion' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="toggleFavouriteState(true)" *ngIf="discussion.canfavourite && !discussion.starred">
<core-icon name="fa-star" item-start></core-icon>
<h2>{{ 'addon.mod_forum.addtofavourites' | translate }}</h2>
</ion-item>
<ion-item text-wrap (click)="toggleFavouriteState(false)" *ngIf="discussion.canfavourite && discussion.starred">
<core-icon name="fa-star" item-start [slash]="true"></core-icon>
<h2>{{ 'addon.mod_forum.removefromfavourites' | translate }}</h2>
</ion-item>

View File

@ -0,0 +1,145 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { NavParams, ViewController } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { AddonModForumProvider } from '../../providers/forum';
/**
* This component is meant to display a popover with the discussion options.
*/
@Component({
selector: 'addon-forum-discussion-options-menu',
templateUrl: 'addon-forum-discussion-options-menu.html'
})
export class AddonForumDiscussionOptionsMenuComponent implements OnInit {
discussion: any; // The discussion.
forumId: number; // The forum Id.
cmId: number; // The component module Id.
canPin = false;
constructor(navParams: NavParams,
protected viewCtrl: ViewController,
protected forumProvider: AddonModForumProvider,
protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.discussion = navParams.get('discussion');
this.forumId = navParams.get('forumId');
this.cmId = navParams.get('cmId');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (this.forumProvider.isSetPinStateAvailableForSite()) {
// Use the canAddDiscussion WS to check if the user can pin discussions.
this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
this.canPin = !!response.canpindiscussions;
}).catch(() => {
this.canPin = false;
});
} else {
this.canPin = false;
}
}
/**
* Lock or unlock the discussion.
*
* @param locked True to lock the discussion, false to unlock.
*/
setLockState(locked: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.forumProvider.setLockState(this.forumId, this.discussion.discussion, locked).then((response) => {
this.viewCtrl.dismiss({action: 'lock', value: locked});
const data = {
forumId: this.forumId,
discussionId: this.discussion.discussion,
cmId: this.cmId,
locked: response.locked
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.lockupdated', true);
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.viewCtrl.dismiss();
}).finally(() => {
modal.dismiss();
});
}
/**
* Pin or unpin the discussion.
*
* @param pinned True to pin the discussion, false to unpin it.
*/
setPinState(pinned: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.forumProvider.setPinState(this.discussion.discussion, pinned).then(() => {
this.viewCtrl.dismiss({action: 'pin', value: pinned});
const data = {
forumId: this.forumId,
discussionId: this.discussion.discussion,
cmId: this.cmId,
pinned: pinned
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.pinupdated', true);
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.viewCtrl.dismiss();
}).finally(() => {
modal.dismiss();
});
}
/**
* Star or unstar the discussion.
*
* @param starred True to star the discussion, false to unstar it.
*/
toggleFavouriteState(starred: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.forumProvider.toggleFavouriteState(this.discussion.discussion, starred).then(() => {
this.viewCtrl.dismiss({action: 'star', value: starred});
const data = {
forumId: this.forumId,
discussionId: this.discussion.discussion,
cmId: this.cmId,
starred: starred
};
this.eventsProvider.trigger(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.showToast('addon.mod_forum.favouriteupdated', true);
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.viewCtrl.dismiss();
}).finally(() => {
modal.dismiss();
});
}
}

View File

@ -43,51 +43,52 @@
</div>
<ng-container *ngFor="let discussion of offlineDiscussions">
<ion-item text-wrap (click)="openNewDiscussion(discussion.timecreated)" [attr.no-lines]="discussion.groupname" [class.core-split-item-selected]="discussion.timecreated == -selectedDiscussion">
<ion-avatar core-user-avatar [user]="discussion" item-start [courseId]="courseId"></ion-avatar>
<h2><core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text></h2>
<h3 *ngIf="discussion.userfullname">
<ion-note float-end padding-left><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</ion-note>
{{discussion.userfullname}}
</h3>
</ion-item>
<ion-item *ngIf="discussion.groupname" (click)="openNewDiscussion(discussion.timecreated)" [class.core-split-item-selected]="discussion.timecreated == -selectedDiscussion">
<ion-note text-end>
<ion-icon name="people"></ion-icon> {{ discussion.groupname }}
</ion-note>
<ion-item text-wrap (click)="openNewDiscussion(discussion.timecreated)" [attr.no-lines]="discussion.groupname" [class.core-split-item-selected]="discussion.timecreated == -selectedDiscussion" class="addon-mod-forum-discussion">
<div class="addon-mod-forum-discussion-title">
<h2>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
</h2>
</div>
<div class="addon-mod-forum-discussion-info">
<ion-avatar core-user-avatar [user]="discussion" item-start [courseId]="courseId" *ngIf="discussion.userfullname"></ion-avatar>
<h3 *ngIf="discussion.userfullname">{{discussion.userfullname}}</h3>
<p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p>
<p><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
</div>
</ion-item>
</ng-container>
<ng-container *ngFor="let discussion of discussions">
<ion-item text-wrap (click)="openDiscussion(discussion)" no-lines [class.core-split-item-selected]="discussion.discussion == selectedDiscussion">
<ion-avatar core-user-avatar [user]="discussion" item-start [courseId]="courseId"></ion-avatar>
<h2>
<core-icon name="fa-map-pin" *ngIf="discussion.pinned"></core-icon>
<core-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"></core-icon>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
</h2>
<h3>
<ion-note float-end padding-left text-end>
<div *ngIf="discussion.numunread"><core-icon name="fa-circle" color="primary"></core-icon> {{ 'addon.mod_forum.unreadpostsnumber' | translate:{ '$a' : discussion.numunread} }}</div>
</ion-note>
{{discussion.userfullname}}
</h3>
<p>{{discussion.created | coreDateDayOrTime}}</p>
</ion-item>
<ion-item (click)="openDiscussion(discussion)" [class.core-split-item-selected]="discussion.discussion == selectedDiscussion">
<ion-row text-center>
<ion-col *ngIf="discussion.groupname">
<ion-item (click)="openDiscussion(discussion)" [class.core-split-item-selected]="discussion.discussion == selectedDiscussion" class="addon-mod-forum-discussion">
<div class="addon-mod-forum-discussion-title">
<h2 text-wrap>
<core-icon name="fa-map-pin" *ngIf="discussion.pinned"></core-icon>
<core-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"></core-icon>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
</h2>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event, discussion)" *ngIf="canPin || discussion.canlock || discussion.canfavourite">
<core-icon name="more"></core-icon>
</button>
</div>
<div class="addon-mod-forum-discussion-info">
<ion-avatar *ngIf="discussion.userfullname" core-user-avatar [user]="discussion" item-start [courseId]="courseId"></ion-avatar>
<div class="addon-mod-forum-discussion-author">
<h3 *ngIf="discussion.userfullname">{{discussion.userfullname}}</h3>
<p *ngIf="discussion.groupname"><ion-icon name="people"></ion-icon> {{ discussion.groupname }}</p>
<p>{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
</div>
</div>
<ion-row text-center class="addon-mod-forum-discussion-more-info">
<ion-col text-start>
<ion-note>
<ion-icon name="people"></ion-icon> {{ discussion.groupname }}
<ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
<ng-container *ngIf="discussion.timemodified > discussion.created">{{discussion.timemodified | coreTimeAgo}}</ng-container>
<ng-container *ngIf="discussion.timemodified <= discussion.created">{{discussion.created | coreTimeAgo}}</ng-container>
</ion-note>
</ion-col>
<ion-col>
<ion-col text-end>
<ion-note>
<ion-icon name="chatboxes"></ion-icon> {{ 'addon.mod_forum.numreplies' | translate:{numreplies: discussion.numreplies} }}
</ion-note>
</ion-col>
<ion-col *ngIf="discussion.timemodified > discussion.created">
<ion-note>
<ion-icon name="time"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }} {{discussion.timemodified | coreTimeAgo}}
<ion-badge text-center *ngIf="discussion.numunread" [attr.aria-label]="'addon.mod_forum.unreadpostsnumber' | translate:{ '$a' : discussion.numunread}">{{ discussion.numunread }}</ion-badge>
</ion-note>
</ion-col>
</ion-row>

View File

@ -1,5 +1,54 @@
$addon-forum-avatar-size: 28px;
ion-app.app-root addon-mod-forum-index {
.addon-forum-star {
color: $core-star-color;
}
.addon-mod-forum-discussion.item {
.label {
margin-top: 4px;
h2 {
margin-top: 8px;
margin-bottom: 8px;
font-weight: bold;
ion-icon {
@include margin(0, 6px, 0, 0);
}
}
h3 {
font-size: 1.6rem;
}
}
ion-avatar {
width: $addon-forum-avatar-size;
height: $addon-forum-avatar-size;
min-width: $addon-forum-avatar-size;
min-height: $addon-forum-avatar-size;
&[item-start] {
@include margin(0, 8px, 0, 0);
}
img {
width: $addon-forum-avatar-size;
height: $addon-forum-avatar-size;
}
}
.addon-mod-forum-discussion-title,
.addon-mod-forum-discussion-info {
display: flex;
align-items: center;
}
.addon-mod-forum-discussion-title h2,
.addon-mod-forum-discussion-info .addon-mod-forum-discussion-author {
flex-grow: 1;
}
.addon-mod-forum-discussion-more-info {
font-size: 1.4rem;
clear: both;
}
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, Optional, Injector, ViewChild } from '@angular/core';
import { Content, ModalController, NavController } from 'ionic-angular';
import { Content, ModalController, NavController, PopoverController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
@ -27,6 +27,7 @@ import { AddonModForumHelperProvider } from '../../providers/helper';
import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumSyncProvider } from '../../providers/sync';
import { AddonModForumPrefetchHandler } from '../../providers/prefetch-handler';
import { AddonForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
/**
* Component that displays a forum entry page.
@ -61,6 +62,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
protected page = 0;
protected trackPosts = false;
protected usesGroups = false;
protected canPin = false;
protected syncManualObserver: any; // It will observe the sync manual event.
protected replyObserver: any;
protected newDiscObserver: any;
@ -83,7 +85,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
protected forumSync: AddonModForumSyncProvider,
protected prefetchDelegate: CoreCourseModulePrefetchDelegate,
protected prefetchHandler: AddonModForumPrefetchHandler,
protected ratingOffline: CoreRatingOfflineProvider) {
protected ratingOffline: CoreRatingOfflineProvider,
protected popoverCtrl: PopoverController) {
super(injector);
this.sortingAvailable = this.forumProvider.isDiscussionListSortingAvailable();
@ -106,8 +109,30 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.eventReceived.bind(this, true));
this.replyObserver = this.eventsProvider.on(AddonModForumProvider.REPLY_DISCUSSION_EVENT,
this.eventReceived.bind(this, false));
this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT,
this.eventReceived.bind(this, false));
this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data) => {
this.forumProvider.invalidateDiscussionsList(this.forum.id).finally(() => {
// If it's a new discussion in tablet mode, try to open it.
if (data.discussionId) {
// Discussion sent to server, search it in the list of discussions.
const discussion = this.discussions.find((disc) => {
return data.discussionId = disc.discussion;
});
if (discussion) {
if (typeof data.locked != 'undefined') {
discussion.locked = data.locked;
}
if (typeof data.pinned != 'undefined') {
discussion.pinned = data.pinned;
}
if (typeof data.starred != 'undefined') {
discussion.starred = data.starred;
}
}
}
});
});
// Select the current opened discussion.
this.viewDiscObserver = this.eventsProvider.on(AddonModForumProvider.VIEW_DISCUSSION_EVENT, (data) => {
@ -211,18 +236,30 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
});
}
}).then(() => {
return Promise.all([
// Check if the activity uses groups.
this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => {
this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS);
}),
this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => {
// Disallow adding discussions if cut-off date is reached and the user has not the capability to override it.
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff;
this.canAddDiscussion = this.forum.cancreatediscussions && !cutoffDateReached;
}),
]);
const promises = [];
// Check if the activity uses groups.
promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => {
this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS);
}));
promises.push(this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => {
// Disallow adding discussions if cut-off date is reached and the user has not the capability to override it.
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff;
this.canAddDiscussion = this.forum.cancreatediscussions && !cutoffDateReached;
}));
if (this.forumProvider.isSetPinStateAvailableForSite()) {
// Use the canAddDiscussion WS to check if the user can pin discussions.
promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id).then((response) => {
this.canPin = !!response.canpindiscussions;
}).catch(() => {
this.canPin = false;
}));
} else {
this.canPin = false;
}
return Promise.all(promises);
}));
promises.push(this.fetchSortOrderPreference());
@ -559,6 +596,42 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.sortOrderSelectorExpanded = true;
}
/**
* Show the context menu.
*
* @param e Click Event.
*/
showOptionsMenu(e: Event, discussion: any): void {
e.preventDefault();
e.stopPropagation();
const popover = this.popoverCtrl.create(AddonForumDiscussionOptionsMenuComponent, {
discussion: discussion,
forumId: this.forum.id,
cmId: this.module.id
});
popover.onDidDismiss((data) => {
if (data && data.action) {
switch (data.action) {
case 'lock':
discussion.locked = data.value;
break;
case 'pin':
discussion.pinned = data.value;
break;
case 'star':
discussion.starred = data.value;
break;
default:
break;
}
}
});
popover.present({
ev: e
});
}
/**
* Component being destroyed.
*/

View File

@ -1,85 +1,91 @@
<ion-card-header text-wrap no-padding id="addon-mod_forum-post-{{post.id}}">
<ion-item text-wrap>
<ion-avatar core-user-avatar [user]="post" item-start></ion-avatar>
<h2>
<core-icon name="fa-map-pin" *ngIf="post.parent == 0 && post.pinned"></core-icon>
<core-icon name="fa-star" class="addon-forum-star" *ngIf="post.parent == 0 && !post.pinned && post.starred"></core-icon>
<span [class.core-bold]="post.parent == 0"><core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text></span>
</h2>
<p>
<ion-note float-end padding-left *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</ion-note>
<ion-note float-end padding-left text-end *ngIf="post.modified">
{{post.modified | coreDateDayOrTime}}
<div *ngIf="trackPosts && !post.postread"><core-icon name="fa-circle" color="primary"></core-icon> {{ 'addon.mod_forum.unread' | translate }}</div>
</ion-note>
{{post.userfullname}}
</p>
</ion-item>
</ion-card-header>
<ion-card-content padding-top>
<div padding-bottom *ngIf="post.isprivatereply">
<ion-note>{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
<div class="addon-mod_forum-post">
<ion-card-header text-wrap no-padding id="addon-mod_forum-post-{{post.id}}">
<ion-item text-wrap>
<div class="addon-mod-forum-post-title" *ngIf="displaySubject">
<h2 text-wrap>
<core-icon name="fa-map-pin" *ngIf="post.parent == 0 && post.pinned"></core-icon>
<core-icon name="fa-star" class="addon-forum-star" *ngIf="post.parent == 0 && !post.pinned && post.starred"></core-icon>
<core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text>
</h2>
</div>
<div class="addon-mod-forum-post-info">
<ion-avatar *ngIf="post.userfullname" core-user-avatar [user]="post" item-start [courseId]="courseId"></ion-avatar>
<div class="addon-mod-forum-post-author">
<h3 *ngIf="post.userfullname">{{post.userfullname}}</h3>
<p *ngIf="post.groupname"><ion-icon name="people"></ion-icon> {{ post.groupname }}</p>
<p *ngIf="post.modified">{{post.modified * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
<p *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
</div>
<ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
<core-icon name="fa-circle" color="primary"></core-icon>
</ion-note>
</div>
</ion-item>
</ion-card-header>
<ion-card-content [attr.padding-top]="post.parent == 0 || null">
<div padding-bottom *ngIf="post.isprivatereply">
<ion-note>{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
</div>
<core-format-text [component]="component" [componentId]="componentId" [text]="post.message" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text>
<div no-lines *ngIf="post.attachments && post.attachments.length > 0">
<core-files [files]="post.attachments" [component]="component" [componentId]="componentId" showInline="true"></core-files>
</div>
</ion-card-content>
<div class="addon-mod-forum-post-more-info">
<ion-item text-wrap *ngIf="tagsEnabled && post.tags && post.tags.length > 0">
<div item-start>{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="post.tags"></core-tag-list>
</ion-item>
<core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.userid" (onUpdate)="ratingUpdated()"></core-rating-rate>
<core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate>
<ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button">
<button ion-button icon-left clear small (click)="showReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
<ion-icon name="undo"></ion-icon> {{ 'addon.mod_forum.reply' | translate }}
</button>
</ion-item>
</div>
<core-format-text [component]="component" [componentId]="componentId" [text]="post.message" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text>
<div no-lines>
<ng-container *ngFor="let attachment of post.attachments">
<!-- Files already attached to the submission. -->
<core-file *ngIf="!attachment.name" [file]="attachment" [component]="component" [componentId]="componentId"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="attachment.name" [file]="attachment"></core-local-file>
<ion-item text-end *ngIf="!post.id && (!replyData.isEditing || replyData.replyingTo != post.parent)">
<button ion-button icon-left clear small (click)="editReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.parent">
<ion-icon name="create"></ion-icon> {{ 'addon.mod_forum.edit' | translate }}
</button>
</ion-item>
<ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)">
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item>
<ion-item text-wrap *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
<ion-checkbox item-end [(ngModel)]="replyData.isprivatereply"></ion-checkbox>
</ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
<core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon>
{{ 'addon.mod_forum.advanced' | translate }}
</ion-item-divider>
<ng-container *ngIf="advanced">
<core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
</ng-container>
</div>
</ion-card-content>
<ion-item text-wrap *ngIf="tagsEnabled && post.tags && post.tags.length > 0">
<div item-start>{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="post.tags"></core-tag-list>
</ion-item>
<core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.userid" (onUpdate)="ratingUpdated()"></core-rating-rate>
<core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate>
<ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button">
<button ion-button icon-left clear small (click)="showReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
<ion-icon name="undo"></ion-icon> {{ 'addon.mod_forum.reply' | translate }}
</button>
</ion-item>
<ion-item text-end *ngIf="!post.id && (!replyData.isEditing || replyData.replyingTo != post.parent)">
<button ion-button icon-left clear small (click)="editReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.parent">
<ion-icon name="create"></ion-icon> {{ 'addon.mod_forum.edit' | translate }}
</button>
</ion-item>
<ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)">
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item>
<ion-item text-wrap *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
<ion-checkbox item-end [(ngModel)]="replyData.isprivatereply"></ion-checkbox>
</ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
<core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon>
{{ 'addon.mod_forum.advanced' | translate }}
</ion-item-divider>
<ng-container *ngIf="advanced">
<core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
</ng-container>
<ion-grid>
<ion-row>
<ion-col>
<button ion-button block (click)="reply()" [disabled]="replyData.subject == '' || replyData.message == null">{{ 'addon.mod_forum.posttoforum' | translate }}</button>
</ion-col>
<ion-col>
<button ion-button block color="light" (click)="cancel()">{{ 'core.cancel' | translate }}</button>
</ion-col>
</ion-row>
<ion-row *ngIf="replyData.isEditing">
<ion-col>
<button ion-button block color="light" (click)="discard()">{{ 'core.discard' | translate }}</button>
</ion-col>
</ion-row>
</ion-grid>
</ion-list>
<ion-grid>
<ion-row>
<ion-col>
<button ion-button block (click)="reply()" [disabled]="replyData.subject == '' || replyData.message == null">{{ 'addon.mod_forum.posttoforum' | translate }}</button>
</ion-col>
<ion-col>
<button ion-button block color="light" (click)="cancel()">{{ 'core.cancel' | translate }}</button>
</ion-col>
</ion-row>
<ion-row *ngIf="replyData.isEditing">
<ion-col>
<button ion-button block color="light" (click)="discard()">{{ 'core.discard' | translate }}</button>
</ion-col>
</ion-row>
</ion-grid>
</ion-list>
</div>

View File

@ -1,5 +1,68 @@
ion-app.app-root addon-mod-forum-post {
ion-app.app-root addon-mod-forum-post .addon-mod_forum-post {
background-color: $white;
border-bottom: 1px solid $list-md-border-color;
.addon-forum-star {
color: $core-star-color;
}
.card-header .item {
.label {
margin-top: 4px;
h2 {
margin-top: 8px;
margin-bottom: 8px;
font-weight: bold;
ion-icon {
@include margin(0, 6px, 0, 0);
}
}
h3 {
font-size: 1.6rem;
}
}
ion-avatar {
width: $addon-forum-avatar-size;
height: $addon-forum-avatar-size;
min-width: $addon-forum-avatar-size;
min-height: $addon-forum-avatar-size;
&[item-start] {
@include margin(0, 8px, 0, 0);
}
img {
width: $addon-forum-avatar-size;
height: $addon-forum-avatar-size;
}
}
.addon-mod-forum-post-title,
.addon-mod-forum-post-info {
display: flex;
align-items: center;
}
.addon-mod-forum-post-info {
margin-top: 8px;
}
.addon-mod-forum-post-title + .addon-mod-forum-post-info {
margin-top: 0px;
}
.addon-mod-forum-post-title h2,
.addon-mod-forum-post-info .addon-mod-forum-post-author {
flex-grow: 1;
}
}
.item .item-inner {
border-bottom: 0;
}
.addon-mod-forum-post-more-info div {
font-size: 1.4rem;
}
}

View File

@ -54,6 +54,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
uniqueId: string;
advanced = false; // Display all form fields.
tagsEnabled: boolean;
displaySubject = true;
protected syncId: string;
@ -78,6 +79,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
*/
ngOnInit(): void {
this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parent;
const reTranslated = this.translate.instant('addon.mod_forum.re');
this.displaySubject = this.post.parent == 0 ||
(this.post.subject != this.defaultSubject && this.post.subject != 'Re: ' + this.defaultSubject &&
this.post.subject != reTranslated + this.defaultSubject);
}
/**

View File

@ -38,12 +38,12 @@
</ion-card>
<ion-card class="core-info-card" icon-start *ngIf="discussion && discussion.locked">
<ion-icon name="information-circle"></ion-icon> {{ 'addon.mod_forum.discussionlocked' | translate }}
<core-icon name="fa-lock"></core-icon> {{ 'addon.mod_forum.discussionlocked' | translate }}
</ion-card>
<ion-card *ngIf="discussion" margin-bottom class="highlight">
<div *ngIf="discussion" margin-bottom class="highlight">
<addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</ion-card>
</div>
<ion-card *ngIf="sort != 'nested'">
<ng-container *ngFor="let post of posts; first as first">

View File

@ -1,5 +1,5 @@
ion-app.app-root page-addon-mod-forum-discussion {
.card.highlight .card-header .item {
.highlight .card-header .item {
background-color: $gray-lighter;
@include darkmode() {
background-color: $black;

View File

@ -89,6 +89,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
hasOfflineRatings: boolean;
protected ratingOfflineObserver: any;
protected ratingSyncObserver: any;
protected changeDiscObserver: any;
constructor(navParams: NavParams,
network: Network,
@ -130,13 +131,17 @@ export class AddonModForumDiscussionPage implements OnDestroy {
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchPosts(true, false, true).then(() => {
if (this.postId) {
// Scroll to the post.
setTimeout(() => {
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_forum-post-' + this.postId);
});
}
this.sitesProvider.getCurrentSite().getLocalSiteConfig('AddonModForumDiscussionSort', this.sort).then((value) => {
this.sort = value;
}).finally(() => {
this.fetchPosts(true, false, true).then(() => {
if (this.postId) {
// Scroll to the post.
setTimeout(() => {
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_forum-post-' + this.postId);
});
}
});
});
}
@ -183,6 +188,20 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.hasOfflineRatings = false;
}
});
this.changeDiscObserver = this.eventsProvider.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data) => {
this.forumProvider.invalidateDiscussionsList(this.forum.id).finally(() => {
if (typeof data.locked != 'undefined') {
this.discussion.locked = data.locked;
}
if (typeof data.pinned != 'undefined') {
this.discussion.pinned = data.pinned;
}
if (typeof data.starred != 'undefined') {
this.discussion.starred = data.starred;
}
});
});
}
/**
@ -498,6 +517,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
changeSort(type: SortType): Promise<any> {
this.discussionLoaded = false;
this.sort = type;
this.sitesProvider.getCurrentSite().setLocalSiteConfig('AddonModForumDiscussionSort', this.sort);
this.domUtils.scrollToTop(this.content);
return this.fetchPosts();
@ -610,6 +630,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.syncManualObserver && this.syncManualObserver.off();
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
this.ratingSyncObserver && this.ratingSyncObserver.off();
this.changeDiscObserver && this.changeDiscObserver.off();
}
/**

View File

@ -35,7 +35,7 @@ export class AddonModForumProvider {
static NEW_DISCUSSION_EVENT = 'addon_mod_forum_new_discussion';
static REPLY_DISCUSSION_EVENT = 'addon_mod_forum_reply_discussion';
static VIEW_DISCUSSION_EVENT = 'addon_mod_forum_view_discussion';
static CHANGE_DISCUSSION_EVENT = 'addon_mod_forum_lock_discussion';
static CHANGE_DISCUSSION_EVENT = 'addon_mod_forum_change_discussion_status';
static MARK_READ_EVENT = 'addon_mod_forum_mark_read';
static PREFERENCE_SORTORDER = 'forum_discussionlistsortorder';

View File

@ -134,6 +134,8 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
return this.resourceHelper.getEmbeddedHtml(this.module, this.courseId).then((html) => {
this.contentText = html;
this.mode = this.contentText.length > 0 ? 'embedded' : 'external';
});
} else {
this.mode = 'external';

View File

@ -53,23 +53,7 @@ export class AddonModResourceHelperProvider {
getEmbeddedHtml(module: any, courseId: number): Promise<any> {
return this.courseHelper.downloadModuleWithMainFileIfNeeded(module, courseId, AddonModResourceProvider.COMPONENT,
module.id, module.contents).then((result) => {
const file = module.contents[0],
ext = this.mimetypeUtils.getFileExtension(file.filename),
type = this.mimetypeUtils.getExtensionType(ext),
mimeType = this.mimetypeUtils.getMimeType(ext);
if (type == 'image') {
return '<img src="' + result.path + '"></img>';
}
if (type == 'audio' || type == 'video') {
return '<' + type + ' controls title="' + file.filename + '"" src="' + result.path + '">' +
'<source src="' + result.path + '" type="' + mimeType + '">' +
'</' + type + '>';
}
// Shouldn't reach here, the user should have called CoreMimetypeUtilsProvider#canBeEmbedded.
return '';
return this.mimetypeUtils.getEmbeddedHtml(module.contents[0], result.path);
});
}

View File

@ -25,12 +25,7 @@
<ion-item text-wrap *ngIf="submission.content">
<core-format-text [component]="component" [componentId]="componentId" [text]="submission.content" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
</ion-item>
<ion-item *ngFor="let attachment of submission.attachmentfiles">
<!-- Files already attached to the submission. -->
<core-file *ngIf="!attachment.name" [file]="attachment" [component]="component" [componentId]="componentId"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="attachment.name" [file]="attachment"></core-local-file>
</ion-item>
<core-files [files]="submission.attachmentfiles" [component]="component" [componentId]="componentId"></core-files>
<ion-item text-wrap *ngIf="viewDetails && submission.feedbackauthor">
<ion-avatar *ngIf="evaluateByProfile" core-user-avatar [user]="evaluateByProfile" item-start [courseId]="courseId" [userId]="evaluateByProfile.id"></ion-avatar>

View File

@ -27,6 +27,7 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
import { CoreSearchBoxComponent } from './search-box/search-box';
import { CoreFileComponent } from './file/file';
import { CoreFilesComponent } from './files/files';
import { CoreIconComponent } from './icon/icon';
import { CoreContextMenuComponent } from './context-menu/context-menu';
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
@ -67,6 +68,7 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
CoreEmptyBoxComponent,
CoreSearchBoxComponent,
CoreFileComponent,
CoreFilesComponent,
CoreIconComponent,
CoreContextMenuComponent,
CoreContextMenuItemComponent,
@ -118,6 +120,7 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
CoreEmptyBoxComponent,
CoreSearchBoxComponent,
CoreFileComponent,
CoreFilesComponent,
CoreIconComponent,
CoreContextMenuComponent,
CoreContextMenuItemComponent,

View File

@ -0,0 +1,9 @@
<ng-container *ngIf="showInline && contentText">
<core-format-text [text]="contentText" [filter]="false"></core-format-text>
</ng-container>
<ng-container *ngFor="let file of files">
<!-- Files already attached to the filearea. -->
<core-file *ngIf="!file.name && !file.embedType" [file]="file" [component]="component" [componentId]="componentId" [alwaysDownload]="alwaysDownload" [canDownload]="canDownload" [showSize]="showSize" [showTime]="showTime"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="file.name && !file.embedType" [file]="file"></core-local-file>
</ng-container>

View File

@ -0,0 +1,82 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, DoCheck, KeyValueDiffers } from '@angular/core';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Component to render a file list.
*
* <core-files [files]="files" [component]="component" [componentId]="assign.cmid">
* </core-files>
*/
@Component({
selector: 'core-files',
templateUrl: 'core-files.html'
})
export class CoreFilesComponent implements OnInit, DoCheck {
@Input() files: any[]; // List of files.
@Input() component: string; // Component the downloaded files will be linked to.
@Input() componentId: string | number; // Component ID.
@Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
// Use it for files that you cannot determine if they're outdated or not.
@Input() canDownload?: boolean | string = true; // Whether file can be downloaded.
@Input() showSize?: boolean | string = true; // Whether show filesize.
@Input() showTime?: boolean | string = true; // Whether show file time modified.
@Input() showInline = false; // If true, it will reorder and try to show inline files first.
contentText: string;
protected differ: any; // To detect changes in the data input.
constructor(protected mimetypeUtils: CoreMimetypeUtilsProvider,
protected utils: CoreUtilsProvider,
differs: KeyValueDiffers) {
this.differ = differs.find([]).create();
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (this.utils.isTrueOrOne(this.showInline)) {
this.renderInlineFiles();
}
}
/**
* Detect and act upon changes that Angular cant or wont detect on its own (objects and arrays).
*/
ngDoCheck(): void {
if (this.utils.isTrueOrOne(this.showInline)) {
// Check if there's any change in the files array.
const changes = this.differ.diff(this.files);
if (changes) {
this.renderInlineFiles();
}
}
}
/**
* Calculate contentText based on fils that can be rendered inline.
*/
protected renderInlineFiles(): void {
this.contentText = this.files.reduce((previous, file) => {
const text = this.mimetypeUtils.getEmbeddedHtml(file);
return text ? previous + '<br>' + text : previous;
}, '');
}
}

View File

@ -138,6 +138,40 @@ export class CoreMimetypeUtilsProvider {
}
}
/**
* Set the embed type to display an embedded file and mimetype if not found.
*
* @param file File object.
* @paran path Alternative path that will override fileurl from file object.
*/
getEmbeddedHtml(file: any, path?: string): string {
let ext;
if (file.mimetype) {
ext = this.getExtension(file.mimetype);
} else {
ext = this.getFileExtension(file.filename);
file.mimetype = this.getMimeType(ext);
}
if (this.canBeEmbedded(ext)) {
file.embedType = this.getExtensionType(ext);
path = path || file.fileurl;
if (file.embedType == 'image') {
return '<img src="' + path + '">';
}
if (file.embedType == 'audio' || file.embedType == 'video') {
return '<' + file.embedType + ' controls title="' + file.filename + '" src="' + path + '">' +
'<source src="' + path + '" type="' + file.mimetype + '">' +
'</' + file.embedType + '>';
}
}
return '';
}
/**
* Get the URL of the icon of an extension.
*