Merge pull request #2036 from albertgasset/MOBILE-2201

Mobile 2201
main
Juan Leyva 2019-08-02 11:28:12 +02:00 committed by GitHub
commit aa3e894248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 2517 additions and 41 deletions

View File

@ -420,6 +420,7 @@
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext", "addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext",
"addon.mod_book.errorchapter": "book", "addon.mod_book.errorchapter": "book",
"addon.mod_book.modulenameplural": "book", "addon.mod_book.modulenameplural": "book",
"addon.mod_book.tagarea_book_chapters": "book",
"addon.mod_book.toc": "book", "addon.mod_book.toc": "book",
"addon.mod_chat.beep": "chat", "addon.mod_chat.beep": "chat",
"addon.mod_chat.chatreport": "chat", "addon.mod_chat.chatreport": "chat",
@ -479,6 +480,7 @@
"addon.mod_data.confirmdeleterecord": "data", "addon.mod_data.confirmdeleterecord": "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.emptyaddform": "data", "addon.mod_data.emptyaddform": "data",
"addon.mod_data.entrieslefttoadd": "data", "addon.mod_data.entrieslefttoadd": "data",
"addon.mod_data.entrieslefttoaddtoview": "data", "addon.mod_data.entrieslefttoaddtoview": "data",
@ -503,8 +505,10 @@
"addon.mod_data.recorddisapproved": "data", "addon.mod_data.recorddisapproved": "data",
"addon.mod_data.resetsettings": "data", "addon.mod_data.resetsettings": "data",
"addon.mod_data.search": "data", "addon.mod_data.search": "data",
"addon.mod_data.searchbytagsnotsupported": "local_moodlemobileapp",
"addon.mod_data.selectedrequired": "data", "addon.mod_data.selectedrequired": "data",
"addon.mod_data.single": "data", "addon.mod_data.single": "data",
"addon.mod_data.tagarea_data_records": "data",
"addon.mod_data.timeadded": "data", "addon.mod_data.timeadded": "data",
"addon.mod_data.timemodified": "data", "addon.mod_data.timemodified": "data",
"addon.mod_data.usedate": "data", "addon.mod_data.usedate": "data",
@ -597,6 +601,7 @@
"addon.mod_forum.reply": "forum", "addon.mod_forum.reply": "forum",
"addon.mod_forum.replyplaceholder": "forum", "addon.mod_forum.replyplaceholder": "forum",
"addon.mod_forum.subject": "forum", "addon.mod_forum.subject": "forum",
"addon.mod_forum.tagarea_forum_posts": "forum",
"addon.mod_forum.thisforumhasduedate": "forum", "addon.mod_forum.thisforumhasduedate": "forum",
"addon.mod_forum.thisforumisdue": "forum", "addon.mod_forum.thisforumisdue": "forum",
"addon.mod_forum.unlockdiscussion": "forum", "addon.mod_forum.unlockdiscussion": "forum",
@ -631,6 +636,7 @@
"addon.mod_glossary.modulenameplural": "glossary", "addon.mod_glossary.modulenameplural": "glossary",
"addon.mod_glossary.noentriesfound": "local_moodlemobileapp", "addon.mod_glossary.noentriesfound": "local_moodlemobileapp",
"addon.mod_glossary.searchquery": "local_moodlemobileapp", "addon.mod_glossary.searchquery": "local_moodlemobileapp",
"addon.mod_glossary.tagarea_glossary_entries": "glossary",
"addon.mod_imscp.deploymenterror": "imscp", "addon.mod_imscp.deploymenterror": "imscp",
"addon.mod_imscp.modulenameplural": "imscp", "addon.mod_imscp.modulenameplural": "imscp",
"addon.mod_imscp.showmoduledescription": "local_moodlemobileapp", "addon.mod_imscp.showmoduledescription": "local_moodlemobileapp",
@ -887,6 +893,7 @@
"addon.mod_wiki.pageexists": "wiki", "addon.mod_wiki.pageexists": "wiki",
"addon.mod_wiki.pagename": "wiki", "addon.mod_wiki.pagename": "wiki",
"addon.mod_wiki.subwiki": "local_moodlemobileapp", "addon.mod_wiki.subwiki": "local_moodlemobileapp",
"addon.mod_wiki.tagarea_wiki_pages": "wiki",
"addon.mod_wiki.titleshouldnotbeempty": "local_moodlemobileapp", "addon.mod_wiki.titleshouldnotbeempty": "local_moodlemobileapp",
"addon.mod_wiki.viewpage": "local_moodlemobileapp", "addon.mod_wiki.viewpage": "local_moodlemobileapp",
"addon.mod_wiki.wikipage": "local_moodlemobileapp", "addon.mod_wiki.wikipage": "local_moodlemobileapp",
@ -1868,6 +1875,20 @@
"core.submit": "moodle", "core.submit": "moodle",
"core.success": "moodle", "core.success": "moodle",
"core.tablet": "local_moodlemobileapp", "core.tablet": "local_moodlemobileapp",
"core.tag.defautltagcoll": "moodle",
"core.tag.errorareanotsupported": "local_moodlemobileapp",
"core.tag.inalltagcoll": "moodle",
"core.tag.itemstaggedwith": "moodle",
"core.tag.notagsfound": "moodle",
"core.tag.searchtags": "moodle",
"core.tag.showingfirsttags": "moodle",
"core.tag.tag": "moodle",
"core.tag.tagarea_course": "moodle",
"core.tag.tagarea_course_modules": "moodle",
"core.tag.tagarea_post": "moodle",
"core.tag.tagarea_user": "moodle",
"core.tag.tags": "moodle",
"core.tag.warningareasnotsupported": "local_moodlemobileapp",
"core.teachers": "moodle", "core.teachers": "moodle",
"core.thereisdatatosync": "local_moodlemobileapp", "core.thereisdatatosync": "local_moodlemobileapp",
"core.thisdirection": "langconfig", "core.thisdirection": "langconfig",

View File

@ -17,12 +17,14 @@ import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreUserDelegate } from '@core/user/providers/user-delegate';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonBlogProvider } from './providers/blog'; import { AddonBlogProvider } from './providers/blog';
import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler'; import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler';
import { AddonBlogUserHandler } from './providers/user-handler'; import { AddonBlogUserHandler } from './providers/user-handler';
import { AddonBlogCourseOptionHandler } from './providers/course-option-handler'; import { AddonBlogCourseOptionHandler } from './providers/course-option-handler';
import { AddonBlogComponentsModule } from './components/components.module'; import { AddonBlogComponentsModule } from './components/components.module';
import { AddonBlogIndexLinkHandler } from './providers/index-link-handler'; import { AddonBlogIndexLinkHandler } from './providers/index-link-handler';
import { AddonBlogTagAreaHandler } from './providers/tag-area-handler';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -35,17 +37,20 @@ import { AddonBlogIndexLinkHandler } from './providers/index-link-handler';
AddonBlogMainMenuHandler, AddonBlogMainMenuHandler,
AddonBlogUserHandler, AddonBlogUserHandler,
AddonBlogCourseOptionHandler, AddonBlogCourseOptionHandler,
AddonBlogIndexLinkHandler AddonBlogIndexLinkHandler,
AddonBlogTagAreaHandler
] ]
}) })
export class AddonBlogModule { export class AddonBlogModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler, constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler,
userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate, userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate,
courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate,
linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate) { linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate,
tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: AddonBlogTagAreaHandler) {
mainMenuDelegate.registerHandler(menuHandler); mainMenuDelegate.registerHandler(menuHandler);
userDelegate.registerHandler(userHandler); userDelegate.registerHandler(userHandler);
courseOptionsDelegate.registerHandler(courseOptionHandler); courseOptionsDelegate.registerHandler(courseOptionHandler);
contentLinksDelegate.registerHandler(linkHandler); contentLinksDelegate.registerHandler(linkHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
} }
} }

View File

@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module'; import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonBlogEntriesComponent } from './entries/entries'; import { AddonBlogEntriesComponent } from './entries/entries';
@NgModule({ @NgModule({
@ -33,7 +34,8 @@ import { AddonBlogEntriesComponent } from './entries/entries';
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CorePipesModule, CorePipesModule,
CoreCommentsComponentsModule CoreCommentsComponentsModule,
CoreTagComponentsModule
], ],
providers: [ providers: [
], ],

View File

@ -29,6 +29,10 @@
</ion-item> </ion-item>
<ion-card-content> <ion-card-content>
<core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id"></core-format-text> <core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id"></core-format-text>
<ion-item text-wrap *ngIf="tagsEnabled && entry.tags && entry.tags.length > 0">
<div item-start>{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="entry.tags"></core-tag-list>
</ion-item>
<ion-item *ngIf="commentsEnabled"> <ion-item *ngIf="commentsEnabled">
<core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid" contextLevel="user"></core-comments> <core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid" contextLevel="user"></core-comments>
</ion-item> </ion-item>

View File

@ -19,6 +19,7 @@ import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserProvider } from '@core/user/providers/user';
import { AddonBlogProvider } from '../../providers/blog'; import { AddonBlogProvider } from '../../providers/blog';
import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreCommentsProvider } from '@core/comments/providers/comments';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Component that displays the blog entries. * Component that displays the blog entries.
@ -49,10 +50,11 @@ export class AddonBlogEntriesComponent implements OnInit {
onlyMyEntries = false; onlyMyEntries = false;
component = AddonBlogProvider.COMPONENT; component = AddonBlogProvider.COMPONENT;
commentsEnabled: boolean; commentsEnabled: boolean;
tagsEnabled: boolean;
constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider, constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider,
protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider,
protected commentsProvider: CoreCommentsProvider) { protected commentsProvider: CoreCommentsProvider, private tagProvider: CoreTagProvider) {
this.currentUserId = sitesProvider.getCurrentSiteUserId(); this.currentUserId = sitesProvider.getCurrentSiteUserId();
} }
@ -85,6 +87,7 @@ export class AddonBlogEntriesComponent implements OnInit {
} }
this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(); this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite();
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
this.fetchEntries().then(() => { this.fetchEntries().then(() => {
this.blogProvider.logView(this.filter).catch(() => { this.blogProvider.logView(this.filter).catch(() => {

View File

@ -0,0 +1,58 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
import { AddonBlogProvider } from './blog';
/**
* Handler to support tags.
*/
@Injectable()
export class AddonBlogTagAreaHandler implements CoreTagAreaHandler {
name = 'AddonBlogTagAreaHandler';
type = 'core/post';
constructor(private tagHelper: CoreTagHelperProvider, private blogProvider: AddonBlogProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.blogProvider.isPluginEnabled();
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
return this.tagHelper.parseFeedContent(content);
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -22,6 +22,8 @@ import { AddonModBookPrefetchHandler } from './providers/prefetch-handler';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModBookTagAreaHandler } from './providers/tag-area-handler';
// List of providers (without handlers). // List of providers (without handlers).
export const ADDON_MOD_BOOK_PROVIDERS: any[] = [ export const ADDON_MOD_BOOK_PROVIDERS: any[] = [
@ -39,18 +41,21 @@ export const ADDON_MOD_BOOK_PROVIDERS: any[] = [
AddonModBookModuleHandler, AddonModBookModuleHandler,
AddonModBookLinkHandler, AddonModBookLinkHandler,
AddonModBookListLinkHandler, AddonModBookListLinkHandler,
AddonModBookPrefetchHandler AddonModBookPrefetchHandler,
AddonModBookTagAreaHandler
] ]
}) })
export class AddonModBookModule { export class AddonModBookModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModBookModuleHandler, constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModBookModuleHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModBookLinkHandler, contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModBookLinkHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModBookPrefetchHandler, prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModBookPrefetchHandler,
listLinkHandler: AddonModBookListLinkHandler) { listLinkHandler: AddonModBookListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
tagAreaHandler: AddonModBookTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler); moduleDelegate.registerHandler(moduleHandler);
contentLinksDelegate.registerHandler(linkHandler); contentLinksDelegate.registerHandler(linkHandler);
contentLinksDelegate.registerHandler(listLinkHandler); contentLinksDelegate.registerHandler(listLinkHandler);
prefetchDelegate.registerHandler(prefetchHandler); prefetchDelegate.registerHandler(prefetchHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
} }
} }

View File

@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModBookIndexComponent } from './index/index'; import { AddonModBookIndexComponent } from './index/index';
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -31,7 +32,8 @@ import { AddonModBookIndexComponent } from './index/index';
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreCourseComponentsModule CoreCourseComponentsModule,
CoreTagComponentsModule
], ],
providers: [ providers: [
], ],

View File

@ -21,6 +21,10 @@
<div padding> <div padding>
<core-navigation-bar [previous]="previousChapter > 0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)"></core-navigation-bar> <core-navigation-bar [previous]="previousChapter > 0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)"></core-navigation-bar>
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent"></core-format-text> <core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent"></core-format-text>
<div margin-top *ngIf="tagsEnabled && contentsMap && contentsMap[currentChapter] && contentsMap[currentChapter].tags && contentsMap[currentChapter].tags.length > 0">
<b>{{ 'core.tag.tags' | translate }}:</b>
<core-tag-list [tags]="contentsMap[currentChapter].tags"></core-tag-list>
</div>
<core-navigation-bar [previous]="previousChapter > 0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)"></core-navigation-bar> <core-navigation-bar [previous]="previousChapter > 0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)"></core-navigation-bar>
</div> </div>

View File

@ -19,6 +19,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book'; import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book';
import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Component that displays a book. * Component that displays a book.
@ -34,6 +35,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
chapterContent: string; chapterContent: string;
previousChapter: string; previousChapter: string;
nextChapter: string; nextChapter: string;
tagsEnabled: boolean;
protected chapters: AddonModBookTocChapter[]; protected chapters: AddonModBookTocChapter[];
protected currentChapter: string; protected currentChapter: string;
@ -41,7 +43,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider,
private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler, private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler,
private modalCtrl: ModalController, @Optional() private content: Content) { private modalCtrl: ModalController, private tagProvider: CoreTagProvider, @Optional() private content: Content) {
super(injector); super(injector);
} }
@ -51,6 +53,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
this.loadContent(); this.loadContent();
} }

View File

@ -1,5 +1,6 @@
{ {
"errorchapter": "Error reading chapter of book.", "errorchapter": "Error reading chapter of book.",
"modulenameplural": "Books", "modulenameplural": "Books",
"tagarea_book_chapters": "Book chapters",
"toc": "Table of contents" "toc": "Table of contents"
} }

View File

@ -24,6 +24,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { CoreTagItem } from '@core/tag/providers/tag';
/** /**
* A book chapter inside the toc list. * A book chapter inside the toc list.
@ -52,7 +53,13 @@ export interface AddonModBookTocChapter {
* Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path * Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path
* is identified by the relative path in the book, and the value is the URL of the file. * is identified by the relative path in the book, and the value is the URL of the file.
*/ */
export type AddonModBookContentsMap = {[chapter: string]: {indexUrl?: string, paths: {[path: string]: string}}}; export type AddonModBookContentsMap = {
[chapter: string]: {
indexUrl?: string,
paths: {[path: string]: string},
tags?: CoreTagItem[]
}
};
/** /**
* Service that provides some features for books. * Service that provides some features for books.
@ -203,8 +210,9 @@ export class AddonModBookProvider {
map[chapter] = map[chapter] || { paths: {} }; map[chapter] = map[chapter] || { paths: {} };
if (content.filename == 'index.html' && filepathIsChapter) { if (content.filename == 'index.html' && filepathIsChapter) {
// Index of the chapter, set indexUrl of the chapter. // Index of the chapter, set indexUrl and tags of the chapter.
map[chapter].indexUrl = content.fileurl; map[chapter].indexUrl = content.fileurl;
map[chapter].tags = content.tags;
} else { } else {
if (filepathIsChapter) { if (filepathIsChapter) {
// It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671). // It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671).

View File

@ -0,0 +1,75 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { AddonModBookProvider } from './book';
/**
* Handler to support tags.
*/
@Injectable()
export class AddonModBookTagAreaHandler implements CoreTagAreaHandler {
name = 'AddonModBookTagAreaHandler';
type = 'mod_book/book_chapters';
constructor(private tagHelper: CoreTagHelperProvider, private bookProvider: AddonModBookProvider,
private courseProvider: CoreCourseProvider, private urlUtils: CoreUrlUtilsProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.bookProvider.isPluginEnabled();
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
const items = this.tagHelper.parseFeedContent(content);
// Find module ids of the returned books, they are needed by the link delegate.
return Promise.all(items.map((item) => {
const params = this.urlUtils.extractUrlParams(item.url);
if (params.b && !params.id) {
const bookId = parseInt(params.b, 10);
return this.courseProvider.getModuleBasicInfoByInstance(bookId, 'book').then((module) => {
item.url += '&id=' + module.id;
});
}
})).then(() => {
return items;
});
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -20,6 +20,7 @@ import { AddonModDataOfflineProvider } from '../../providers/offline';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserProvider } from '@core/user/providers/user';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Component that displays a database action. * Component that displays a database action.
@ -41,13 +42,16 @@ export class AddonModDataActionComponent implements OnInit {
rootUrl: string; rootUrl: string;
url: string; url: string;
userPicture: string; userPicture: string;
tagsEnabled: boolean;
constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider, protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider,
sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, private navCtrl: NavController, sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, private navCtrl: NavController,
protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider) { protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider,
private tagProvider: CoreTagProvider) {
this.rootUrl = sitesProvider.getCurrentSite().getURL(); this.rootUrl = sitesProvider.getCurrentSite().getURL();
this.siteId = sitesProvider.getCurrentSiteId(); this.siteId = sitesProvider.getCurrentSiteId();
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
} }
/** /**

View File

@ -32,3 +32,5 @@
</a> </a>
<a *ngIf="action == 'user' && entry" core-user-link [courseId]="database.courseid" [userId]="entry.userid" [title]="entry.fullname">{{entry.fullname}}</a> <a *ngIf="action == 'user' && entry" core-user-link [courseId]="database.courseid" [userId]="entry.userid" [title]="entry.fullname">{{entry.fullname}}</a>
<core-tag-list *ngIf="tagsEnabled && action == 'tags' && entry" [tags]="entry.tags"></core-tag-list>

View File

@ -25,6 +25,7 @@ import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin';
import { AddonModDataActionComponent } from './action/action'; import { AddonModDataActionComponent } from './action/action';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -41,7 +42,8 @@ import { CoreCommentsComponentsModule } from '@core/comments/components/componen
CorePipesModule, CorePipesModule,
CoreCourseComponentsModule, CoreCourseComponentsModule,
CoreCompileHtmlComponentModule, CoreCompileHtmlComponentModule,
CoreCommentsComponentsModule CoreCommentsComponentsModule,
CoreTagComponentsModule
], ],
providers: [ providers: [
], ],

View File

@ -33,6 +33,8 @@ import { AddonModDataSyncCronHandler } from './providers/sync-cron-handler';
import { AddonModDataOfflineProvider } from './providers/offline'; import { AddonModDataOfflineProvider } from './providers/offline';
import { AddonModDataFieldsDelegate } from './providers/fields-delegate'; import { AddonModDataFieldsDelegate } from './providers/fields-delegate';
import { AddonModDataDefaultFieldHandler } from './providers/default-field-handler'; import { AddonModDataDefaultFieldHandler } from './providers/default-field-handler';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModDataTagAreaHandler } from './providers/tag-area-handler';
import { AddonModDataFieldModule } from './fields/field.module'; import { AddonModDataFieldModule } from './fields/field.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager'; import { CoreUpdateManagerProvider } from '@providers/update-manager';
@ -67,7 +69,8 @@ export const ADDON_MOD_DATA_PROVIDERS: any[] = [
AddonModDataEditLinkHandler, AddonModDataEditLinkHandler,
AddonModDataListLinkHandler, AddonModDataListLinkHandler,
AddonModDataSyncCronHandler, AddonModDataSyncCronHandler,
AddonModDataDefaultFieldHandler AddonModDataDefaultFieldHandler,
AddonModDataTagAreaHandler
] ]
}) })
export class AddonModDataModule { export class AddonModDataModule {
@ -77,7 +80,8 @@ export class AddonModDataModule {
cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler, updateManager: CoreUpdateManagerProvider, cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler, updateManager: CoreUpdateManagerProvider,
approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler, approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler,
showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler, showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler,
listLinkHandler: AddonModDataListLinkHandler) { listLinkHandler: AddonModDataListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
tagAreaHandler: AddonModDataTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler); moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler); prefetchDelegate.registerHandler(prefetchHandler);
@ -88,6 +92,7 @@ export class AddonModDataModule {
contentLinksDelegate.registerHandler(editLinkHandler); contentLinksDelegate.registerHandler(editLinkHandler);
contentLinksDelegate.registerHandler(listLinkHandler); contentLinksDelegate.registerHandler(listLinkHandler);
cronDelegate.register(syncHandler); cronDelegate.register(syncHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema. // Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({ updateManager.registerSiteTableMigration({

View File

@ -10,6 +10,7 @@
"confirmdeleterecord": "Are you sure you want to delete this entry?", "confirmdeleterecord": "Are you sure you want to delete this entry?",
"descending": "Descending", "descending": "Descending",
"disapprove": "Undo approval", "disapprove": "Undo approval",
"edittagsnotsupported": "Sorry, editing tags is not supported by the app.",
"emptyaddform": "You did not fill out any fields!", "emptyaddform": "You did not fill out any fields!",
"entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity", "entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
"entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.", "entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
@ -34,8 +35,10 @@
"recorddisapproved": "Entry unapproved", "recorddisapproved": "Entry unapproved",
"resetsettings": "Reset filters", "resetsettings": "Reset filters",
"search": "Search", "search": "Search",
"searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.",
"selectedrequired": "All selected required", "selectedrequired": "All selected required",
"single": "View single", "single": "View single",
"tagarea_data_records": "Data records",
"timeadded": "Time added", "timeadded": "Time added",
"timemodified": "Time modified", "timemodified": "Time modified",
"usedate": "Include in search." "usedate": "Include in search."

View File

@ -28,6 +28,7 @@ import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataOfflineProvider } from '../../providers/offline';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataComponentsModule } from '../../components/components.module'; import { AddonModDataComponentsModule } from '../../components/components.module';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Page that displays the view edit page. * Page that displays the view edit page.
@ -68,7 +69,8 @@ export class AddonModDataEditPage {
protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider,
sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected translate: TranslateService, sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected translate: TranslateService,
protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider) { protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider,
private tagProvider: CoreTagProvider) {
this.module = params.get('module') || {}; this.module = params.get('module') || {};
this.entryId = params.get('entryId') || null; this.entryId = params.get('entryId') || null;
this.courseId = params.get('courseId'); this.courseId = params.get('courseId');
@ -302,6 +304,11 @@ export class AddonModDataEditPage {
template = template.replace(replace, 'field_' + field.id); template = template.replace(replace, 'field_' + field.id);
}); });
// Editing tags is not supported.
replace = new RegExp('##tags##', 'gi');
const message = '<p class="item-dimmed">{{ \'addon.mod_data.edittagsnotsupported\' | translate }}</p>';
template = template.replace(replace, this.tagProvider.areTagsAvailableInSite() ? message : '');
return template; return template;
} }

View File

@ -21,6 +21,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModDataComponentsModule } from '../../components/components.module'; import { AddonModDataComponentsModule } from '../../components/components.module';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataHelperProvider } from '../../providers/helper';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Page that displays the search modal. * Page that displays the search modal.
@ -42,7 +43,8 @@ export class AddonModDataSearchPage {
constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider, constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider) { protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider,
private tagProvider: CoreTagProvider) {
this.search = params.get('search'); this.search = params.get('search');
this.fields = params.get('fields'); this.fields = params.get('fields');
this.data = params.get('data'); this.data = params.get('data');
@ -117,9 +119,10 @@ export class AddonModDataSearchPage {
[placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname"></ion-input></span>'; [placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname"></ion-input></span>';
template = template.replace(replace, render); template = template.replace(replace, render);
// Tags are unsupported right now. // Searching by tags is not supported.
replace = new RegExp('##tags##', 'gi'); replace = new RegExp('##tags##', 'gi');
template = template.replace(replace, ''); const message = '<p class="item-dimmed">{{ \'addon.mod_data.searchbytagsnotsupported\' | translate }}</p>';
template = template.replace(replace, this.tagProvider.areTagsAvailableInSite() ? message : '');
return template; return template;
} }

View File

@ -367,6 +367,7 @@ export class AddonModDataHelperProvider {
userpicture: true, userpicture: true,
timeadded: true, timeadded: true,
timemodified: true, timemodified: true,
tags: true,
edit: record.canmanageentry && !record.deleted, // This already checks capabilities and readonly period. edit: record.canmanageentry && !record.deleted, // This already checks capabilities and readonly period.
delete: record.canmanageentry, delete: record.canmanageentry,
@ -377,7 +378,6 @@ export class AddonModDataHelperProvider {
comments: database.comments, comments: database.comments,
// Unsupported actions. // Unsupported actions.
tags: false,
delcheck: false, delcheck: false,
export: false export: false
}; };

View File

@ -0,0 +1,58 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
import { AddonModDataProvider } from './data';
/**
* Handler to support tags.
*/
@Injectable()
export class AddonModDataTagAreaHandler implements CoreTagAreaHandler {
name = 'AddonModDataTagAreaHandler';
type = 'mod_data/data_records';
constructor(private tagHelper: CoreTagHelperProvider, private dataProvider: AddonModDataProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.dataProvider.isPluginEnabled();
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
return this.tagHelper.parseFeedContent(content);
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -21,6 +21,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module'; import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { CoreRatingComponentsModule } from '@core/rating/components/components.module'; import { CoreRatingComponentsModule } from '@core/rating/components/components.module';
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModForumIndexComponent } from './index/index'; import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post'; import { AddonModForumPostComponent } from './post/post';
@ -37,7 +38,8 @@ import { AddonModForumPostComponent } from './post/post';
CoreDirectivesModule, CoreDirectivesModule,
CorePipesModule, CorePipesModule,
CoreCourseComponentsModule, CoreCourseComponentsModule,
CoreRatingComponentsModule CoreRatingComponentsModule,
CoreTagComponentsModule
], ],
providers: [ providers: [
], ],

View File

@ -30,6 +30,10 @@
</ng-container> </ng-container>
</div> </div>
</ion-card-content> </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-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> <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"> <ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button">

View File

@ -25,6 +25,7 @@ import { AddonModForumHelperProvider } from '../../providers/helper';
import { AddonModForumOfflineProvider } from '../../providers/offline'; import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumSyncProvider } from '../../providers/sync'; import { AddonModForumSyncProvider } from '../../providers/sync';
import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreRatingInfo } from '@core/rating/providers/rating';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
@ -52,6 +53,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
uniqueId: string; uniqueId: string;
advanced = false; // Display all form fields. advanced = false; // Display all form fields.
tagsEnabled: boolean;
protected syncId: string; protected syncId: string;
@ -65,8 +67,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
private forumHelper: AddonModForumHelperProvider, private forumHelper: AddonModForumHelperProvider,
private forumOffline: AddonModForumOfflineProvider, private forumOffline: AddonModForumOfflineProvider,
private forumSync: AddonModForumSyncProvider, private forumSync: AddonModForumSyncProvider,
private tagProvider: CoreTagProvider,
@Optional() private content: Content) { @Optional() private content: Content) {
this.onPostChange = new EventEmitter<void>(); this.onPostChange = new EventEmitter<void>();
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
} }
/** /**

View File

@ -18,6 +18,7 @@ import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers/delegate'; import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers/delegate';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModForumProvider } from './providers/forum'; import { AddonModForumProvider } from './providers/forum';
import { AddonModForumOfflineProvider } from './providers/offline'; import { AddonModForumOfflineProvider } from './providers/offline';
import { AddonModForumHelperProvider } from './providers/helper'; import { AddonModForumHelperProvider } from './providers/helper';
@ -30,6 +31,7 @@ import { AddonModForumDiscussionLinkHandler } from './providers/discussion-link-
import { AddonModForumListLinkHandler } from './providers/list-link-handler'; import { AddonModForumListLinkHandler } from './providers/list-link-handler';
import { AddonModForumPostLinkHandler } from './providers/post-link-handler'; import { AddonModForumPostLinkHandler } from './providers/post-link-handler';
import { AddonModForumPushClickHandler } from './providers/push-click-handler'; import { AddonModForumPushClickHandler } from './providers/push-click-handler';
import { AddonModForumTagAreaHandler } from './providers/tag-area-handler';
import { AddonModForumComponentsModule } from './components/components.module'; import { AddonModForumComponentsModule } from './components/components.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager'; import { CoreUpdateManagerProvider } from '@providers/update-manager';
@ -59,7 +61,8 @@ export const ADDON_MOD_FORUM_PROVIDERS: any[] = [
AddonModForumListLinkHandler, AddonModForumListLinkHandler,
AddonModForumPostLinkHandler, AddonModForumPostLinkHandler,
AddonModForumDiscussionLinkHandler, AddonModForumDiscussionLinkHandler,
AddonModForumPushClickHandler AddonModForumPushClickHandler,
AddonModForumTagAreaHandler
] ]
}) })
export class AddonModForumModule { export class AddonModForumModule {
@ -69,7 +72,8 @@ export class AddonModForumModule {
indexHandler: AddonModForumIndexLinkHandler, discussionHandler: AddonModForumDiscussionLinkHandler, indexHandler: AddonModForumIndexLinkHandler, discussionHandler: AddonModForumDiscussionLinkHandler,
updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModForumListLinkHandler, updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModForumListLinkHandler,
pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler, pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler,
postLinkHandler: AddonModForumPostLinkHandler) { postLinkHandler: AddonModForumPostLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
tagAreaHandler: AddonModForumTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler); moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler); prefetchDelegate.registerHandler(prefetchHandler);
@ -79,6 +83,7 @@ export class AddonModForumModule {
linksDelegate.registerHandler(listLinkHandler); linksDelegate.registerHandler(listLinkHandler);
linksDelegate.registerHandler(postLinkHandler); linksDelegate.registerHandler(postLinkHandler);
pushNotificationsDelegate.registerClickHandler(pushClickHandler); pushNotificationsDelegate.registerClickHandler(pushClickHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema. // Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTablesMigration([ updateManager.registerSiteTablesMigration([

View File

@ -50,6 +50,7 @@
"reply": "Reply", "reply": "Reply",
"replyplaceholder": "Write your reply...", "replyplaceholder": "Write your reply...",
"subject": "Subject", "subject": "Subject",
"tagarea_forum_posts": "Forum posts",
"thisforumhasduedate": "The due date for posting to this forum is {{$a}}.", "thisforumhasduedate": "The due date for posting to this forum is {{$a}}.",
"thisforumisdue": "The due date for posting to this forum was {{$a}}.", "thisforumisdue": "The due date for posting to this forum was {{$a}}.",
"unlockdiscussion": "Unlock this discussion", "unlockdiscussion": "Unlock this discussion",

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
/**
* Handler to support tags.
*/
@Injectable()
export class AddonModForumTagAreaHandler implements CoreTagAreaHandler {
name = 'AddonModForumTagAreaHandler';
type = 'mod_forum/forum_posts';
constructor(private tagHelper: CoreTagHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
return this.tagHelper.parseFeedContent(content);
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModGlossaryProvider } from './providers/glossary'; import { AddonModGlossaryProvider } from './providers/glossary';
import { AddonModGlossaryOfflineProvider } from './providers/offline'; import { AddonModGlossaryOfflineProvider } from './providers/offline';
import { AddonModGlossaryHelperProvider } from './providers/helper'; import { AddonModGlossaryHelperProvider } from './providers/helper';
@ -28,6 +29,7 @@ import { AddonModGlossaryIndexLinkHandler } from './providers/index-link-handler
import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler'; import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler';
import { AddonModGlossaryListLinkHandler } from './providers/list-link-handler'; import { AddonModGlossaryListLinkHandler } from './providers/list-link-handler';
import { AddonModGlossaryEditLinkHandler } from './providers/edit-link-handler'; import { AddonModGlossaryEditLinkHandler } from './providers/edit-link-handler';
import { AddonModGlossaryTagAreaHandler } from './providers/tag-area-handler';
import { AddonModGlossaryComponentsModule } from './components/components.module'; import { AddonModGlossaryComponentsModule } from './components/components.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager'; import { CoreUpdateManagerProvider } from '@providers/update-manager';
@ -56,7 +58,8 @@ export const ADDON_MOD_GLOSSARY_PROVIDERS: any[] = [
AddonModGlossaryIndexLinkHandler, AddonModGlossaryIndexLinkHandler,
AddonModGlossaryEntryLinkHandler, AddonModGlossaryEntryLinkHandler,
AddonModGlossaryListLinkHandler, AddonModGlossaryListLinkHandler,
AddonModGlossaryEditLinkHandler AddonModGlossaryEditLinkHandler,
AddonModGlossaryTagAreaHandler
] ]
}) })
export class AddonModGlossaryModule { export class AddonModGlossaryModule {
@ -65,7 +68,8 @@ export class AddonModGlossaryModule {
cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate, cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler, indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler,
updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler, updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler,
editLinkHandler: AddonModGlossaryEditLinkHandler) { editLinkHandler: AddonModGlossaryEditLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
tagAreaHandler: AddonModGlossaryTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler); moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler); prefetchDelegate.registerHandler(prefetchHandler);
@ -74,6 +78,7 @@ export class AddonModGlossaryModule {
linksDelegate.registerHandler(discussionHandler); linksDelegate.registerHandler(discussionHandler);
linksDelegate.registerHandler(listLinkHandler); linksDelegate.registerHandler(listLinkHandler);
linksDelegate.registerHandler(editLinkHandler); linksDelegate.registerHandler(editLinkHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema. // Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({ updateManager.registerSiteTableMigration({

View File

@ -26,5 +26,6 @@
"linking": "Auto-linking", "linking": "Auto-linking",
"modulenameplural": "Glossaries", "modulenameplural": "Glossaries",
"noentriesfound": "No entries were found.", "noentriesfound": "No entries were found.",
"searchquery": "Search query" "searchquery": "Search query",
"tagarea_glossary_entries": "Glossary entries"
} }

View File

@ -28,6 +28,10 @@
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId"></core-file> <core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId"></core-file>
</div> </div>
</ng-container> </ng-container>
<ion-item text-wrap *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
<div item-start>{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="entry.tags"></core-tag-list>
</ion-item>
<ion-item text-wrap *ngIf="entry.approved != 1"> <ion-item text-wrap *ngIf="entry.approved != 1">
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p> <p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
</ion-item> </ion-item>

View File

@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module'; import { CorePipesModule } from '@pipes/pipes.module';
import { CoreRatingComponentsModule } from '@core/rating/components/components.module'; import { CoreRatingComponentsModule } from '@core/rating/components/components.module';
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModGlossaryEntryPage } from './entry'; import { AddonModGlossaryEntryPage } from './entry';
@NgModule({ @NgModule({
@ -31,7 +32,8 @@ import { AddonModGlossaryEntryPage } from './entry';
CorePipesModule, CorePipesModule,
IonicPageModule.forChild(AddonModGlossaryEntryPage), IonicPageModule.forChild(AddonModGlossaryEntryPage),
TranslateModule.forChild(), TranslateModule.forChild(),
CoreRatingComponentsModule CoreRatingComponentsModule,
CoreTagComponentsModule
], ],
}) })
export class AddonModForumDiscussionPageModule {} export class AddonModForumDiscussionPageModule {}

View File

@ -16,6 +16,7 @@ import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular'; import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreRatingInfo } from '@core/rating/providers/rating';
import { CoreTagProvider } from '@core/tag/providers/tag';
import { AddonModGlossaryProvider } from '../../providers/glossary'; import { AddonModGlossaryProvider } from '../../providers/glossary';
/** /**
@ -35,15 +36,18 @@ export class AddonModGlossaryEntryPage {
showAuthor = false; showAuthor = false;
showDate = false; showDate = false;
ratingInfo: CoreRatingInfo; ratingInfo: CoreRatingInfo;
tagsEnabled: boolean;
protected courseId: number; protected courseId: number;
protected entryId: number; protected entryId: number;
constructor(navParams: NavParams, constructor(navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private domUtils: CoreDomUtilsProvider,
private glossaryProvider: AddonModGlossaryProvider) { private glossaryProvider: AddonModGlossaryProvider,
private tagProvider: CoreTagProvider) {
this.courseId = navParams.get('courseId'); this.courseId = navParams.get('courseId');
this.entryId = navParams.get('entryId'); this.entryId = navParams.get('entryId');
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
} }
/** /**

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
/**
* Handler to support tags.
*/
@Injectable()
export class AddonModGlossaryTagAreaHandler implements CoreTagAreaHandler {
name = 'AddonModGlossaryTagAreaHandler';
type = 'mod_glossary/glossary_entries';
constructor(private tagHelper: CoreTagHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
return this.tagHelper.parseFeedContent(content);
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -19,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModWikiIndexComponent } from './index/index'; import { AddonModWikiIndexComponent } from './index/index';
import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-picker'; import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-picker';
@ -33,7 +34,8 @@ import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-pic
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreCourseComponentsModule CoreCourseComponentsModule,
CoreTagComponentsModule
], ],
providers: [ providers: [
], ],

View File

@ -50,6 +50,11 @@
<core-format-text *ngIf="pageContent" [component]="component" [componentId]="componentId" [text]="pageContent"></core-format-text> <core-format-text *ngIf="pageContent" [component]="component" [componentId]="componentId" [text]="pageContent"></core-format-text>
<core-empty-box *ngIf="!pageContent" icon="document" [message]="'addon.mod_wiki.nocontent' | translate" [inline]="true"></core-empty-box> <core-empty-box *ngIf="!pageContent" icon="document" [message]="'addon.mod_wiki.nocontent' | translate" [inline]="true"></core-empty-box>
</article> </article>
<div margin-top *ngIf="tagsEnabled && currentPageObj && currentPageObj.tags && currentPageObj.tags.length > 0">
<b>{{ 'core.tag.tags' | translate }}:</b>
<core-tag-list [tags]="currentPageObj.tags"></core-tag-list>
</div>
</div> </div>
</ng-template> </ng-template>
</core-tab> </core-tab>

View File

@ -23,6 +23,7 @@ import { AddonModWikiOfflineProvider } from '../../providers/wiki-offline';
import { AddonModWikiSyncProvider } from '../../providers/wiki-sync'; import { AddonModWikiSyncProvider } from '../../providers/wiki-sync';
import { CoreTabsComponent } from '@components/tabs/tabs'; import { CoreTabsComponent } from '@components/tabs/tabs';
import { AddonModWikiSubwikiPickerComponent } from '../../components/subwiki-picker/subwiki-picker'; import { AddonModWikiSubwikiPickerComponent } from '../../components/subwiki-picker/subwiki-picker';
import { CoreTagProvider } from '@core/tag/providers/tag';
/** /**
* Component that displays a wiki entry page. * Component that displays a wiki entry page.
@ -64,6 +65,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
subwikis: [], subwikis: [],
count: 0 count: 0
}; };
tagsEnabled: boolean;
protected syncEventName = AddonModWikiSyncProvider.AUTO_SYNCED; protected syncEventName = AddonModWikiSyncProvider.AUTO_SYNCED;
protected currentSubwiki: any; // Current selected subwiki. protected currentSubwiki: any; // Current selected subwiki.
@ -81,10 +83,12 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
constructor(injector: Injector, protected wikiProvider: AddonModWikiProvider, @Optional() protected content: Content, constructor(injector: Injector, protected wikiProvider: AddonModWikiProvider, @Optional() protected content: Content,
protected wikiOffline: AddonModWikiOfflineProvider, protected wikiSync: AddonModWikiSyncProvider, protected wikiOffline: AddonModWikiOfflineProvider, protected wikiSync: AddonModWikiSyncProvider,
protected navCtrl: NavController, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, protected navCtrl: NavController, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
protected userProvider: CoreUserProvider, private popoverCtrl: PopoverController) { protected userProvider: CoreUserProvider, private popoverCtrl: PopoverController,
private tagProvider: CoreTagProvider) {
super(injector, content); super(injector, content);
this.pageStr = this.translate.instant('addon.mod_wiki.wikipage'); this.pageStr = this.translate.instant('addon.mod_wiki.wikipage');
this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
} }
/** /**

View File

@ -14,6 +14,7 @@
"pageexists": "This page already exists.", "pageexists": "This page already exists.",
"pagename": "Page name", "pagename": "Page name",
"subwiki": "Sub-wiki", "subwiki": "Sub-wiki",
"tagarea_wiki_pages": "Wiki pages",
"titleshouldnotbeempty": "The title should not be empty", "titleshouldnotbeempty": "The title should not be empty",
"viewpage": "View page", "viewpage": "View page",
"wikipage": "Wiki page", "wikipage": "Wiki page",

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
/**
* Handler to support tags.
*/
@Injectable()
export class AddonModWikiTagAreaHandler implements CoreTagAreaHandler {
name = 'AddonModWikiTagAreaHandler';
type = 'mod_wiki/wiki_pages';
constructor(private tagHelper: CoreTagHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
return this.tagHelper.parseFeedContent(content);
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModWikiComponentsModule } from './components/components.module'; import { AddonModWikiComponentsModule } from './components/components.module';
import { AddonModWikiProvider } from './providers/wiki'; import { AddonModWikiProvider } from './providers/wiki';
import { AddonModWikiOfflineProvider } from './providers/wiki-offline'; import { AddonModWikiOfflineProvider } from './providers/wiki-offline';
@ -29,6 +30,7 @@ import { AddonModWikiPageOrMapLinkHandler } from './providers/page-or-map-link-h
import { AddonModWikiCreateLinkHandler } from './providers/create-link-handler'; import { AddonModWikiCreateLinkHandler } from './providers/create-link-handler';
import { AddonModWikiEditLinkHandler } from './providers/edit-link-handler'; import { AddonModWikiEditLinkHandler } from './providers/edit-link-handler';
import { AddonModWikiListLinkHandler } from './providers/list-link-handler'; import { AddonModWikiListLinkHandler } from './providers/list-link-handler';
import { AddonModWikiTagAreaHandler } from './providers/tag-area-handler';
import { CoreUpdateManagerProvider } from '@providers/update-manager'; import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers). // List of providers (without handlers).
@ -55,7 +57,8 @@ export const ADDON_MOD_WIKI_PROVIDERS: any[] = [
AddonModWikiPageOrMapLinkHandler, AddonModWikiPageOrMapLinkHandler,
AddonModWikiCreateLinkHandler, AddonModWikiCreateLinkHandler,
AddonModWikiEditLinkHandler, AddonModWikiEditLinkHandler,
AddonModWikiListLinkHandler AddonModWikiListLinkHandler,
AddonModWikiTagAreaHandler
] ]
}) })
export class AddonModWikiModule { export class AddonModWikiModule {
@ -64,7 +67,8 @@ export class AddonModWikiModule {
cronDelegate: CoreCronDelegate, syncHandler: AddonModWikiSyncCronHandler, linksDelegate: CoreContentLinksDelegate, cronDelegate: CoreCronDelegate, syncHandler: AddonModWikiSyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModWikiIndexLinkHandler, pageOrMapHandler: AddonModWikiPageOrMapLinkHandler, indexHandler: AddonModWikiIndexLinkHandler, pageOrMapHandler: AddonModWikiPageOrMapLinkHandler,
createHandler: AddonModWikiCreateLinkHandler, editHandler: AddonModWikiEditLinkHandler, createHandler: AddonModWikiCreateLinkHandler, editHandler: AddonModWikiEditLinkHandler,
updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModWikiListLinkHandler) { updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModWikiListLinkHandler,
tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: AddonModWikiTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler); moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler); prefetchDelegate.registerHandler(prefetchHandler);
@ -74,6 +78,7 @@ export class AddonModWikiModule {
linksDelegate.registerHandler(createHandler); linksDelegate.registerHandler(createHandler);
linksDelegate.registerHandler(editHandler); linksDelegate.registerHandler(editHandler);
linksDelegate.registerHandler(listLinkHandler); linksDelegate.registerHandler(listLinkHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema. // Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({ updateManager.registerSiteTableMigration({

View File

@ -81,6 +81,7 @@ import { CoreQuestionModule } from '@core/question/question.module';
import { CoreCommentsModule } from '@core/comments/comments.module'; import { CoreCommentsModule } from '@core/comments/comments.module';
import { CoreBlockModule } from '@core/block/block.module'; import { CoreBlockModule } from '@core/block/block.module';
import { CoreRatingModule } from '@core/rating/rating.module'; import { CoreRatingModule } from '@core/rating/rating.module';
import { CoreTagModule } from '@core/tag/tag.module';
// Addon modules. // Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module'; import { AddonBadgesModule } from '@addon/badges/badges.module';
@ -225,6 +226,7 @@ export const WP_PROVIDER: any = null;
CoreBlockModule, CoreBlockModule,
CoreRatingModule, CoreRatingModule,
CorePushNotificationsModule, CorePushNotificationsModule,
CoreTagModule,
AddonBadgesModule, AddonBadgesModule,
AddonBlogModule, AddonBlogModule,
AddonCalendarModule, AddonCalendarModule,

View File

@ -419,6 +419,7 @@
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again.", "addon.mod_assign_submission_onlinetext.wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again.",
"addon.mod_book.errorchapter": "Error reading chapter of book.", "addon.mod_book.errorchapter": "Error reading chapter of book.",
"addon.mod_book.modulenameplural": "Books", "addon.mod_book.modulenameplural": "Books",
"addon.mod_book.tagarea_book_chapters": "Book chapters",
"addon.mod_book.toc": "Table of contents", "addon.mod_book.toc": "Table of contents",
"addon.mod_chat.beep": "Beep", "addon.mod_chat.beep": "Beep",
"addon.mod_chat.chatreport": "Chat sessions", "addon.mod_chat.chatreport": "Chat sessions",
@ -478,6 +479,7 @@
"addon.mod_data.confirmdeleterecord": "Are you sure you want to delete this entry?", "addon.mod_data.confirmdeleterecord": "Are you sure you want to delete this entry?",
"addon.mod_data.descending": "Descending", "addon.mod_data.descending": "Descending",
"addon.mod_data.disapprove": "Undo approval", "addon.mod_data.disapprove": "Undo approval",
"addon.mod_data.edittagsnotsupported": "Sorry, editing tags is not supported by the app.",
"addon.mod_data.emptyaddform": "You did not fill out any fields!", "addon.mod_data.emptyaddform": "You did not fill out any fields!",
"addon.mod_data.entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity", "addon.mod_data.entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
"addon.mod_data.entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.", "addon.mod_data.entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
@ -502,8 +504,10 @@
"addon.mod_data.recorddisapproved": "Entry unapproved", "addon.mod_data.recorddisapproved": "Entry unapproved",
"addon.mod_data.resetsettings": "Reset filters", "addon.mod_data.resetsettings": "Reset filters",
"addon.mod_data.search": "Search", "addon.mod_data.search": "Search",
"addon.mod_data.searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.",
"addon.mod_data.selectedrequired": "All selected required", "addon.mod_data.selectedrequired": "All selected required",
"addon.mod_data.single": "View single", "addon.mod_data.single": "View single",
"addon.mod_data.tagarea_data_records": "Data records",
"addon.mod_data.timeadded": "Time added", "addon.mod_data.timeadded": "Time added",
"addon.mod_data.timemodified": "Time modified", "addon.mod_data.timemodified": "Time modified",
"addon.mod_data.usedate": "Include in search.", "addon.mod_data.usedate": "Include in search.",
@ -596,6 +600,7 @@
"addon.mod_forum.reply": "Reply", "addon.mod_forum.reply": "Reply",
"addon.mod_forum.replyplaceholder": "Write your reply...", "addon.mod_forum.replyplaceholder": "Write your reply...",
"addon.mod_forum.subject": "Subject", "addon.mod_forum.subject": "Subject",
"addon.mod_forum.tagarea_forum_posts": "Forum posts",
"addon.mod_forum.thisforumhasduedate": "The due date for posting to this forum is {{$a}}.", "addon.mod_forum.thisforumhasduedate": "The due date for posting to this forum is {{$a}}.",
"addon.mod_forum.thisforumisdue": "The due date for posting to this forum was {{$a}}.", "addon.mod_forum.thisforumisdue": "The due date for posting to this forum was {{$a}}.",
"addon.mod_forum.unlockdiscussion": "Unlock this discussion", "addon.mod_forum.unlockdiscussion": "Unlock this discussion",
@ -630,6 +635,7 @@
"addon.mod_glossary.modulenameplural": "Glossaries", "addon.mod_glossary.modulenameplural": "Glossaries",
"addon.mod_glossary.noentriesfound": "No entries were found.", "addon.mod_glossary.noentriesfound": "No entries were found.",
"addon.mod_glossary.searchquery": "Search query", "addon.mod_glossary.searchquery": "Search query",
"addon.mod_glossary.tagarea_glossary_entries": "Glossary entries",
"addon.mod_imscp.deploymenterror": "Content package error!", "addon.mod_imscp.deploymenterror": "Content package error!",
"addon.mod_imscp.modulenameplural": "IMS content packages", "addon.mod_imscp.modulenameplural": "IMS content packages",
"addon.mod_imscp.showmoduledescription": "Show description", "addon.mod_imscp.showmoduledescription": "Show description",
@ -886,6 +892,7 @@
"addon.mod_wiki.pageexists": "This page already exists.", "addon.mod_wiki.pageexists": "This page already exists.",
"addon.mod_wiki.pagename": "Page name", "addon.mod_wiki.pagename": "Page name",
"addon.mod_wiki.subwiki": "Sub-wiki", "addon.mod_wiki.subwiki": "Sub-wiki",
"addon.mod_wiki.tagarea_wiki_pages": "Wiki pages",
"addon.mod_wiki.titleshouldnotbeempty": "The title should not be empty", "addon.mod_wiki.titleshouldnotbeempty": "The title should not be empty",
"addon.mod_wiki.viewpage": "View page", "addon.mod_wiki.viewpage": "View page",
"addon.mod_wiki.wikipage": "Wiki page", "addon.mod_wiki.wikipage": "Wiki page",
@ -1863,6 +1870,20 @@
"core.submit": "Submit", "core.submit": "Submit",
"core.success": "Success", "core.success": "Success",
"core.tablet": "Tablet", "core.tablet": "Tablet",
"core.tag.defautltagcoll": "Default collection",
"core.tag.errorareanotsupported": "This tag area is not supported by the app.",
"core.tag.inalltagcoll": "Everywhere",
"core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
"core.tag.notagsfound": "No tags matching \"{{$a}}\" found",
"core.tag.searchtags": "Search tags",
"core.tag.showingfirsttags": "Showing {{$a}} most popular tags",
"core.tag.tag": "Tag",
"core.tag.tagarea_course": "Courses",
"core.tag.tagarea_course_modules": "Activities and resources",
"core.tag.tagarea_post": "Blog posts",
"core.tag.tagarea_user": "User interests",
"core.tag.tags": "Tags",
"core.tag.warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app.",
"core.teachers": "Teachers", "core.teachers": "Teachers",
"core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.",
"core.thisdirection": "ltr", "core.thisdirection": "ltr",

View File

@ -39,6 +39,7 @@ export class CoreSearchBoxComponent implements OnInit {
@Input() lengthCheck = 3; // Check value length before submit. If 0, any string will be submitted. @Input() lengthCheck = 3; // Check value length before submit. If 0, any string will be submitted.
@Input() showClear = true; // Show/hide clear button. @Input() showClear = true; // Show/hide clear button.
@Input() disabled = false; // Disables the input text. @Input() disabled = false; // Disables the input text.
@Input() initialSearch: string; // Initial search text.
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form. @Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
@Output() onClear: EventEmitter<void>; // Send event when clearing the search form. @Output() onClear: EventEmitter<void>; // Send event when clearing the search form.
@ -55,6 +56,7 @@ export class CoreSearchBoxComponent implements OnInit {
this.placeholder = this.placeholder || this.translate.instant('core.search'); this.placeholder = this.placeholder || this.translate.instant('core.search');
this.spellcheck = this.utils.isTrueOrOne(this.spellcheck); this.spellcheck = this.utils.isTrueOrOne(this.spellcheck);
this.showClear = this.utils.isTrueOrOne(this.showClear); this.showClear = this.utils.isTrueOrOne(this.showClear);
this.searchText = this.initialSearch || '';
} }
/** /**

View File

@ -23,6 +23,7 @@ import { CoreCourseFormatComponent } from './format/format';
import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleComponent } from './module/module';
import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
import { CoreCourseTagAreaComponent } from './tag-area/tag-area';
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
@NgModule({ @NgModule({
@ -31,6 +32,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup
CoreCourseModuleComponent, CoreCourseModuleComponent,
CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionComponent,
CoreCourseModuleDescriptionComponent, CoreCourseModuleDescriptionComponent,
CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent CoreCourseUnsupportedModuleComponent
], ],
imports: [ imports: [
@ -48,10 +50,12 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup
CoreCourseModuleComponent, CoreCourseModuleComponent,
CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionComponent,
CoreCourseModuleDescriptionComponent, CoreCourseModuleDescriptionComponent,
CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent CoreCourseUnsupportedModuleComponent
], ],
entryComponents: [ entryComponents: [
CoreCourseUnsupportedModuleComponent CoreCourseUnsupportedModuleComponent,
CoreCourseTagAreaComponent
] ]
}) })
export class CoreCourseComponentsModule {} export class CoreCourseComponentsModule {}

View File

@ -0,0 +1,5 @@
<a ion-item text-wrap *ngFor="let item of items" (click)="openCourse(item.courseId)" [title]="item.courseName">
<core-icon name="fa-graduation-cap" item-start></core-icon>
<h2>{{ item.courseName }}</h2>
<p *ngIf="item.categoryName">{{ 'core.category' | translate }}: {{ item.categoryName }}</p>
</a>

View File

@ -0,0 +1,43 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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, Optional } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
/**
* Component that renders the course tag area.
*/
@Component({
selector: 'core-course-tag-area',
templateUrl: 'core-course-tag-area.html'
})
export class CoreCourseTagAreaComponent {
@Input() items: any[]; // Area items to render.
constructor(private navCtrl: NavController, @Optional() private splitviewCtrl: CoreSplitViewComponent,
private courseHelper: CoreCourseHelperProvider) {}
/**
* Open a course.
*
* @param {number} courseId The course to open.
*/
openCourse(courseId: number): void {
// If this component is inside a split view, use the master nav to open it.
const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl;
this.courseHelper.getAndOpenCourse(navCtrl, courseId);
}
}

View File

@ -33,6 +33,9 @@ import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module';
import { CoreCourseSyncProvider } from './providers/sync'; import { CoreCourseSyncProvider } from './providers/sync';
import { CoreCourseSyncCronHandler } from './providers/sync-cron-handler'; import { CoreCourseSyncCronHandler } from './providers/sync-cron-handler';
import { CoreCourseLogCronHandler } from './providers/log-cron-handler'; import { CoreCourseLogCronHandler } from './providers/log-cron-handler';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { CoreCourseTagAreaHandler } from './providers/course-tag-area-handler';
import { CoreCourseModulesTagAreaHandler } from './providers/modules-tag-area-handler';
// List of providers (without handlers). // List of providers (without handlers).
export const CORE_COURSE_PROVIDERS: any[] = [ export const CORE_COURSE_PROVIDERS: any[] = [
@ -68,15 +71,20 @@ export const CORE_COURSE_PROVIDERS: any[] = [
CoreCourseFormatDefaultHandler, CoreCourseFormatDefaultHandler,
CoreCourseModuleDefaultHandler, CoreCourseModuleDefaultHandler,
CoreCourseSyncCronHandler, CoreCourseSyncCronHandler,
CoreCourseLogCronHandler CoreCourseLogCronHandler,
CoreCourseTagAreaHandler,
CoreCourseModulesTagAreaHandler
], ],
exports: [] exports: []
}) })
export class CoreCourseModule { export class CoreCourseModule {
constructor(cronDelegate: CoreCronDelegate, syncHandler: CoreCourseSyncCronHandler, logHandler: CoreCourseLogCronHandler, constructor(cronDelegate: CoreCronDelegate, syncHandler: CoreCourseSyncCronHandler, logHandler: CoreCourseLogCronHandler,
platform: Platform, eventsProvider: CoreEventsProvider) { platform: Platform, eventsProvider: CoreEventsProvider, tagAreaDelegate: CoreTagAreaDelegate,
courseTagAreaHandler: CoreCourseTagAreaHandler, modulesTagAreaHandler: CoreCourseModulesTagAreaHandler) {
cronDelegate.register(syncHandler); cronDelegate.register(syncHandler);
cronDelegate.register(logHandler); cronDelegate.register(logHandler);
tagAreaDelegate.registerHandler(courseTagAreaHandler);
tagAreaDelegate.registerHandler(modulesTagAreaHandler);
platform.resume.subscribe(() => { platform.resume.subscribe(() => {
// Log the app is open to keep user in online status. // Log the app is open to keep user in online status.

View File

@ -0,0 +1,74 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreCourseTagAreaComponent } from '../components/tag-area/tag-area';
/**
* Handler to support tags.
*/
@Injectable()
export class CoreCourseTagAreaHandler implements CoreTagAreaHandler {
name = 'CoreCourseTagAreaHandler';
type = 'core/course';
constructor(private domUtils: CoreDomUtilsProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
const items = [];
const element = this.domUtils.convertToElement(content);
Array.from(element.querySelectorAll('div.coursebox')).forEach((coursebox) => {
const courseId = parseInt(coursebox.getAttribute('data-courseid'), 10);
const courseLink = coursebox.querySelector('.coursename > a');
const categoryLink = coursebox.querySelector('.coursecat > a');
if (courseId > 0 && courseLink) {
items.push({
courseId,
courseName: courseLink.innerHTML,
categoryName: categoryLink ? categoryLink.innerHTML : null
});
}
});
return items;
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreCourseTagAreaComponent;
}
}

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreTagHelperProvider } from '@core/tag/providers/helper';
import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
/**
* Handler to support tags.
*/
@Injectable()
export class CoreCourseModulesTagAreaHandler implements CoreTagAreaHandler {
name = 'CoreCourseModulesTagAreaHandler';
type = 'core/course_modules';
constructor(protected tagHelper: CoreTagHelperProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
return this.tagHelper.parseFeedContent(content);
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreTagFeedComponent;
}
}

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreTagFeedComponent } from './feed/feed';
import { CoreTagListComponent } from './list/list';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagFeedComponent,
CoreTagListComponent
],
imports: [
CommonModule,
IonicModule,
CoreDirectivesModule,
TranslateModule.forChild()
],
providers: [
],
exports: [
CoreTagFeedComponent,
CoreTagListComponent
],
entryComponents: [
CoreTagFeedComponent
]
})
export class CoreTagComponentsModule {}

View File

@ -0,0 +1,8 @@
<a ion-item text-wrap *ngFor="let item of items" [href]="item.url" core-link [capture]="true">
<ion-avatar item-start *ngIf="item.avatarUrl">
<img [src]="item.avatarUrl" core-external-content alt="" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<img item-start *ngIf="item.iconUrl" [src]="item.iconUrl" core-external-content alt="" role="presentation" class="core-module-icon">
<h2>{{ item.heading }}</h2>
<p *ngFor="let text of item.details">{{ text }}</p>
</a>

View File

@ -0,0 +1,26 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
/**
* Component to render a tag area that uses the "core_tag/tagfeed" web template.
*/
@Component({
selector: 'core-tag-feed',
templateUrl: 'core-tag-feed.html'
})
export class CoreTagFeedComponent {
@Input() items: any[]; // Area items to render.
}

View File

@ -0,0 +1,3 @@
<ng-container *ngFor="let tag of tags">
<ion-badge (click)="openTag(tag)" class="core-tag-list-tag">{{ tag.rawname }}</ion-badge>
</ng-container>

View File

@ -0,0 +1,7 @@
ion-app.app-root core-tag-list {
line-height: 1.6;
ion-badge {
cursor: pointer;
}
}

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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, Optional } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreTagItem } from '@core/tag/providers/tag';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
/**
* Component that displays the list of tags of an item.
*/
@Component({
selector: 'core-tag-list',
templateUrl: 'core-tag-list.html'
})
export class CoreTagListComponent {
@Input() tags: CoreTagItem[];
constructor(private navCtrl: NavController, @Optional() private svComponent: CoreSplitViewComponent) {}
/**
* Go to tag index page.
*/
openTag(tag: CoreTagItem): void {
const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
const params = {
tagId: tag.id,
tagName: tag.rawname,
collectionId: tag.tagcollid,
fromContextId: tag.taginstancecontextid
};
navCtrl.push('CoreTagIndexPage', params);
}
}

View File

@ -0,0 +1,16 @@
{
"defautltagcoll": "Default collection",
"errorareanotsupported": "This tag area is not supported by the app.",
"inalltagcoll": "Everywhere",
"itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
"notagsfound": "No tags matching \"{{$a}}\" found",
"searchtags": "Search tags",
"showingfirsttags": "Showing {{$a}} most popular tags",
"tag": "Tag",
"tagarea_course": "Courses",
"tagarea_course_modules": "Activities and resources",
"tagarea_post": "Blog posts",
"tagarea_user": "User interests",
"tags": "Tags",
"warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app."
}

View File

@ -0,0 +1,16 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.tag.itemstaggedwith' | translate: { $a: {tagarea: areaNameKey | translate, tag: tagName} } }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="loaded">
<core-dynamic-component [component]="areaComponent" [data]="{items: items}"></core-dynamic-component>
</ng-container>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreTagIndexAreaPage } from './index-area';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagIndexAreaPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreTagIndexAreaPage),
TranslateModule.forChild()
],
})
export class CoreTagIndexAreaPageModule {}

View File

@ -0,0 +1,150 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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, Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTagProvider } from '@core/tag/providers/tag';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
/**
* Page that displays the tag index area.
*/
@IonicPage({ segment: 'core-tag-index-area' })
@Component({
selector: 'page-core-tag-index-area',
templateUrl: 'index-area.html',
})
export class CoreTagIndexAreaPage {
tagId: number;
tagName: string;
collectionId: number;
areaId: number;
fromContextId: number;
contextId: number;
recursive: boolean;
areaNameKey: string;
loaded = false;
componentName: string;
itemType: string;
items = [];
nextPage = 0;
canLoadMore = false;
areaComponent: any;
loadMoreError = false;
constructor(navParams: NavParams, private injector: Injector, private translate: TranslateService,
private tagProvider: CoreTagProvider, private domUtils: CoreDomUtilsProvider,
private tagAreaDelegate: CoreTagAreaDelegate) {
this.tagId = navParams.get('tagId');
this.tagName = navParams.get('tagName');
this.collectionId = navParams.get('collectionId');
this.areaId = navParams.get('areaId');
this.fromContextId = navParams.get('fromContextId');
this.contextId = navParams.get('contextId');
this.recursive = navParams.get('recursive');
this.areaNameKey = navParams.get('areaNameKey');
// Pass the the following parameters to avoid fetching the first page.
this.componentName = navParams.get('componentName');
this.itemType = navParams.get('itemType');
this.items = navParams.get('items') || [];
this.nextPage = navParams.get('nextPage') || 0;
this.canLoadMore = !!navParams.get('canLoadMore');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
let promise: Promise<any>;
if (!this.componentName || !this.itemType || !this.items.length || this.nextPage == 0) {
promise = this.fetchData(true);
} else {
promise = Promise.resolve();
}
promise.then(() => {
return this.tagAreaDelegate.getComponent(this.componentName, this.itemType, this.injector).then((component) => {
this.areaComponent = component;
});
}).finally(() => {
this.loaded = true;
});
}
/**
* Fetch next page of the tag index area.
*
* @param {boolean} [refresh=false] Whether to refresh the data or fetch a new page.
* @return {Promise<any>} Resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
this.loadMoreError = false;
const page = refresh ? 0 : this.nextPage;
return this.tagProvider.getTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive, page).then((areas) => {
const area = areas[0];
return this.tagAreaDelegate.parseContent(area.component, area.itemtype, area.content).then((items) => {
if (!items || !items.length) {
// Tag area not supported.
return Promise.reject(this.translate.instant('core.tag.errorareanotsupported'));
}
if (page == 0) {
this.items = items;
} else {
this.items.push(...items);
}
this.componentName = area.component;
this.itemType = area.itemtype;
this.areaNameKey = this.tagAreaDelegate.getDisplayNameKey(area.component, area.itemtype);
this.canLoadMore = !!area.nextpageurl;
this.nextPage = page + 1;
});
}).catch((error) => {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
this.domUtils.showErrorModalDefault(error, 'Error loading tag index');
});
}
/**
* Load more items.
*
* @param {any} infiniteComplete Infinite scroll complete function.
* @return {Promise<any>} Resolved when done.
*/
loadMore(infiniteComplete: any): Promise<any> {
return this.fetchData().finally(() => {
infiniteComplete();
});
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshData(refresher: any): void {
this.tagProvider.invalidateTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive).finally(() => {
this.fetchData(true).finally(() => {
refresher.complete();
});
});
}
}

View File

@ -0,0 +1,24 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.tag.tag' | translate }}: {{ tagName }}</ion-title>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item text-wrap *ngIf="hasUnsupportedAreas" class="core-warning-item">
<ion-icon item-start name="warning" color="warning"></ion-icon>
{{ 'core.tag.warningareasnotsupported' | translate }}
</ion-item>
<a ion-item text-wrap *ngFor="let area of areas" [title]="area.nameKey | translate" (click)="openArea(area)" [class.core-split-item-selected]="area.id == selectedAreaId">
<h2>{{ area.nameKey | translate }}</h2>
<ion-badge item-end *ngIf="area.badge">{{ area.badge }}</ion-badge>
</a>
</ion-list>
</core-loading>
</ion-content>
</core-split-view>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreTagIndexPage } from './index';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagIndexPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreTagIndexPage),
TranslateModule.forChild()
],
})
export class CoreTagIndexPageModule {}

View File

@ -0,0 +1,154 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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, ViewChild } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreTagProvider } from '@core/tag/providers/tag';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
/**
* Page that displays the tag index.
*/
@IonicPage({ segment: 'core-tag-index' })
@Component({
selector: 'page-core-tag-index',
templateUrl: 'index.html',
})
export class CoreTagIndexPage {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
tagId: number;
tagName: string;
collectionId: number;
areaId: number;
fromContextId: number;
contextId: number;
recursive: boolean;
loaded = false;
areas: Array<{
id: number,
componentName: string,
itemType: string,
nameKey: string,
items: any[],
canLoadMore: boolean,
badge: string
}>;
selectedAreaId: number;
hasUnsupportedAreas = false;
constructor(navParams: NavParams, private tagProvider: CoreTagProvider, private domUtils: CoreDomUtilsProvider,
private tagAreaDelegate: CoreTagAreaDelegate) {
this.tagId = navParams.get('tagId') || 0;
this.tagName = navParams.get('tagName') || '';
this.collectionId = navParams.get('collectionId');
this.areaId = navParams.get('areaId') || 0;
this.fromContextId = navParams.get('fromContextId') || 0;
this.contextId = navParams.get('contextId') || 0;
this.recursive = navParams.get('recursive') || true;
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchData().then(() => {
if (this.splitviewCtrl.isOn() && this.areas && this.areas.length > 0) {
const area = this.areas.find((area) => area.id == this.areaId);
this.openArea(area || this.areas[0]);
}
}).finally(() => {
this.loaded = true;
});
}
/**
* Fetch first page of tag index per area.
*
* @return {Promise<any>} Resolved when done.
*/
fetchData(): Promise<any> {
return this.tagProvider.getTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive, 0).then((areas) => {
this.areas = [];
this.hasUnsupportedAreas = false;
return Promise.all(areas.map((area) => {
return this.tagAreaDelegate.parseContent(area.component, area.itemtype, area.content).then((items) => {
if (!items || !items.length) {
// Tag area not supported, skip.
this.hasUnsupportedAreas = true;
return null;
}
return {
id: area.ta,
componentName: area.component,
itemType: area.itemtype,
nameKey: this.tagAreaDelegate.getDisplayNameKey(area.component, area.itemtype),
items,
canLoadMore: !!area.nextpageurl,
badge: items && items.length ? items.length + (area.nextpageurl ? '+' : '') : '',
};
});
})).then((areas) => {
this.areas = areas.filter((area) => area != null);
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading tag index');
});
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshData(refresher: any): void {
this.tagProvider.invalidateTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive).finally(() => {
this.fetchData().finally(() => {
refresher.complete();
});
});
}
/**
* Navigate to an index area.
*
* @param {any} area Area.
*/
openArea(area: any): void {
this.selectedAreaId = area.id;
const params = {
tagId: this.tagId,
tagName: this.tagName,
collectionId: this.collectionId,
areaId: area.id,
fromContextId: this.fromContextId,
contextId: this.contextId,
recursive: this.recursive,
areaNameKey: area.nameKey,
componentName: area.component,
itemType: area.itemType,
items: area.items.slice(),
canLoadMore: area.canLoadMore,
nextPage: 1
};
this.splitviewCtrl.push('CoreTagIndexAreaPage', params);
}
}

View File

@ -0,0 +1,37 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.tag.searchtags' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<ion-grid>
<ion-row>
<ion-col col-12 [attr.col-sm-6]="collections && collections.length > 1 ? '' : null">
<core-search-box (onSubmit)="searchTags($event)" (onClear)="searchTags('')" [initialSearch]="query" [disabled]="searching" autocorrect="off" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="0"></core-search-box>
</ion-col>
<ion-col col-12 col-sm-6 *ngIf="collections && collections.length > 1">
<ion-select text-start [(ngModel)]="collectionId" (ngModelChange)="searchTags(query)" [disabled]="searching" interface="popover" class="core-button-select">
<ion-option [value]="0">{{ 'core.tag.inalltagcoll' | translate }}</ion-option>
<ion-option *ngFor="let collection of collections" [value]="collection.id">{{ collection.name }}</ion-option>
</ion-select>
</ion-col>
</ion-row>
</ion-grid>
<core-loading [hideUntil]="loaded && !searching">
<core-empty-box *ngIf="!cloud || !cloud.tags || !cloud.tags.length" icon="pricetags" [message]="'core.tag.notagsfound' | translate: {$a: query}"></core-empty-box>
<ng-container *ngIf="cloud && cloud.tags && cloud.tags.length > 0">
<div text-center class="core-tag-cloud">
<ion-badge *ngFor="let tag of cloud.tags" (click)="openTag(tag)" text-wrap>
<span [class]="'size' + tag.size" >{{ tag.name }}</span>
</ion-badge>
</div>
<p *ngIf="cloud.tags.length < cloud.totalcount" text-center>
{{ 'core.tag.showingfirsttags' | translate: {$a: cloud.tags.length} }}
</p>
</ng-container>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreTagSearchPage } from './search';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagSearchPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreTagSearchPage),
TranslateModule.forChild()
],
})
export class CoreTagSerchPageModule {}

View File

@ -0,0 +1,95 @@
ion-app.app-root page-core-tag-search {
core-search-box ion-card {
width: 100% !important;
margin: 0 !important;
}
.core-tag-cloud ion-badge {
margin: 8px;
cursor: pointer;
.size20 {
font-size: 3.4rem;
}
.size19 {
font-size: 3.3rem;
}
.size18 {
font-size: 3.2rem;
}
.size17 {
font-size: 3.1rem;
}
.size16 {
font-size: 3rem;
}
.size15 {
font-size: 2.9rem;
}
.size14 {
font-size: 2.8rem;
}
.size13 {
font-size: 2.7rem;
}
.size12 {
font-size: 2.6rem;
}
.size11 {
font-size: 2.5rem;
}
.size10 {
font-size: 2.4rem;
}
.size9 {
font-size: 2.3rem;
}
.size8 {
font-size: 2.2rem;
}
.size7 {
font-size: 2.1rem;
}
.size6 {
font-size: 2rem;
}
.size5 {
font-size: 1.9rem;
}
.size4 {
font-size: 1.8rem;
}
.size3 {
font-size: 1.7rem;
}
.size2 {
font-size: 1.6rem;
}
.size1 {
font-size: 1.5rem;
}
.size0 {
font-size: 1.4rem;
}
}
}

View File

@ -0,0 +1,135 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { IonicPage, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreTagProvider, CoreTagCloud, CoreTagCollection, CoreTagCloudTag } from '@core/tag/providers/tag';
/**
* Page that displays most used tags and allows searching.
*/
@IonicPage({ segment: 'core-tag-search' })
@Component({
selector: 'page-core-tag-search',
templateUrl: 'search.html',
})
export class CoreTagSearchPage {
collectionId: number;
query: string;
collections: CoreTagCollection[] = [];
cloud: CoreTagCloud;
loaded = false;
searching = false;
constructor(private navCtrl: NavController, navParams: NavParams, private appProvider: CoreAppProvider,
private translate: TranslateService, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider, private contentLinksHelper: CoreContentLinksHelperProvider,
private tagProvider: CoreTagProvider) {
this.collectionId = navParams.get('collectionId') || 0;
this.query = navParams.get('query') || '';
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchData().finally(() => {
this.loaded = true;
});
}
fetchData(): Promise<any> {
return Promise.all([
this.fetchCollections(),
this.fetchTags()
]).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading tags.');
});
}
/**
* Fetch tag collections.
*
* @return {Promise<any>} Resolved when done.
*/
fetchCollections(): Promise<any> {
return this.tagProvider.getTagCollections().then((collections) => {
collections.forEach((collection) => {
if (!collection.name && collection.isdefault) {
collection.name = this.translate.instant('core.tag.defautltagcoll');
}
});
this.collections = collections;
});
}
/**
* Fetch tags.
*
* @return {Promise<any>} Resolved when done.
*/
fetchTags(): Promise<any> {
return this.tagProvider.getTagCloud(this.collectionId, undefined, undefined, this.query).then((cloud) => {
this.cloud = cloud;
});
}
/**
* Go to tag index page.
*/
openTag(tag: CoreTagCloudTag): void {
const url = this.textUtils.decodeURI(tag.viewurl);
this.contentLinksHelper.handleLink(url, undefined, this.navCtrl);
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshData(refresher: any): void {
this.utils.allPromises([
this.tagProvider.invalidateTagCollections(),
this.tagProvider.invalidateTagCloud(this.collectionId, undefined, undefined, this.query),
]).finally(() => {
return this.fetchData().finally(() => {
refresher.complete();
});
});
}
/**
* Search tags.
*
* @param {string} query Search query.
* @return {Promise<any>} Resolved when done.
*/
searchTags(query: string): Promise<any> {
this.searching = true;
this.query = query;
this.appProvider.closeKeyboard();
return this.fetchTags().catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading tags.');
}).finally(() => {
this.searching = false;
});
}
}

View File

@ -0,0 +1,98 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
/**
* Interface that all tag area handlers must implement.
*/
export interface CoreTagAreaHandler extends CoreDelegateHandler {
/**
* Component and item type separated by a slash. E.g. 'core/course_modules'.
* @type {string}
*/
type: string;
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]>;
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any>;
}
/**
* Delegate to register tag area handlers.
*/
@Injectable()
export class CoreTagAreaDelegate extends CoreDelegate {
protected handlerNameProperty = 'type';
constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
super('CoreTagAreaDelegate', logger, sitesProvider, eventsProvider);
}
/**
* Returns the display name string for this area.
*
* @param {string} component Component name.
* @param {string} itemType Item type.
* @return {string} String key.
*/
getDisplayNameKey(component: string, itemType: string): string {
return (component == 'core' ? 'core.tag' : 'addon.' + component) + '.tagarea_' + itemType;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} component Component name.
* @param {string} itemType Item type.
* @param {string} content Rendered content.
* @return {Promise<any[]>} Promise resolved with the area items, or undefined if not found.
*/
parseContent(component: string, itemType: string, content: string): Promise<any[]> {
const type = component + '/' + itemType;
return Promise.resolve(this.executeFunctionOnEnabled(type, 'parseContent', [content]));
}
/**
* Get the component to use to display an area item.
*
* @param {string} component Component name.
* @param {string} itemType Item type.
* @param {Injector} injector Injector.
* @return {Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(component: string, itemType: string, injector: Injector): Promise<any> {
const type = component + '/' + itemType;
return Promise.resolve(this.executeFunctionOnEnabled(type, 'getComponent', [injector]));
}
}

View File

@ -0,0 +1,81 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Service with helper functions for tags.
*/
@Injectable()
export class CoreTagHelperProvider {
constructor(protected domUtils: CoreDomUtilsProvider) {}
/**
* Parses the rendered content of the "core_tag/tagfeed" web template and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]} Area items.
*/
parseFeedContent(content: string): any[] {
const items = [];
const element = this.domUtils.convertToElement(content);
Array.from(element.querySelectorAll('ul.tag_feed > li.media')).forEach((itemElement) => {
const item: any = { details: [] };
Array.from(itemElement.querySelectorAll('div.media-body > div')).forEach((div: HTMLElement) => {
if (div.classList.contains('media-heading')) {
item.heading = div.innerText.trim();
const link = div.querySelector('a');
if (link) {
item.url = link.getAttribute('href');
}
} else {
// Separate details by lines.
const lines = [''];
Array.from(div.childNodes).forEach((childNode: Node) => {
if (childNode.nodeType == Node.TEXT_NODE) {
lines[lines.length - 1] += childNode.textContent;
} else if (childNode.nodeType == Node.ELEMENT_NODE) {
const childElement = childNode as HTMLElement;
if (childElement.tagName == 'BR') {
lines.push('');
} else {
lines[lines.length - 1] += childElement.innerText;
}
}
});
item.details.push(...lines.map((line) => line.trim()).filter((line) => line != ''));
}
});
const image = itemElement.querySelector('div.itemimage img');
if (image) {
if (image.classList.contains('userpicture')) {
item.avatarUrl = image.getAttribute('src');
} else {
item.iconUrl = image.getAttribute('src');
}
}
if (item.heading && item.url) {
items.push(item);
}
});
return items;
}
}

View File

@ -0,0 +1,81 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreTagProvider } from './tag';
/**
* Handler to treat links to tag index.
*/
@Injectable()
export class CoreTagIndexLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreTagIndexLinkHandler';
pattern = /\/tag\/index\.php/;
constructor(private tagProvider: CoreTagProvider, private linkHelper: CoreContentLinksHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @param {any} [data] Extra data to handle the URL.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number, data?: any):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const pageParams = {
tagId: parseInt(params.id, 10) || 0,
tagName: params.tag || '',
collectionId: parseInt(params.tc, 10) || 0,
areaId: parseInt(params.ta, 10) || 0,
fromContextId: parseInt(params.from, 10) || 0,
contextId: parseInt(params.ctx, 10) || 0,
recursive: parseInt(params.rec, 10) || 1
};
if (!pageParams.tagId && (!pageParams.tagName || !pageParams.collectionId)) {
this.linkHelper.goInSite(navCtrl, 'CoreTagSearchPage', {}, siteId);
} else if (pageParams.areaId) {
this.linkHelper.goInSite(navCtrl, 'CoreTagIndexAreaPage', pageParams, siteId);
} else {
this.linkHelper.goInSite(navCtrl, 'CoreTagIndexPage', pageParams, siteId);
}
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
return this.tagProvider.areTagsAvailable(siteId);
}
}

View File

@ -0,0 +1,59 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreTagProvider } from './tag';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Handler to inject an option into main menu.
*/
@Injectable()
export class CoreTagMainMenuHandler implements CoreMainMenuHandler {
name = 'CoreTag';
priority = 300;
constructor(private tagProvider: CoreTagProvider, private utils: CoreUtilsProvider) { }
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.tagProvider.areTagsAvailable().then((available) => {
if (!available) {
return false;
}
// The only way to check whether tags are enabled on web is to perform a WS call.
return this.utils.promiseWorks(this.tagProvider.getTagCollections());
});
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
return {
icon: 'pricetags',
title: 'core.tag.tags',
page: 'CoreTagSearchPage',
class: 'core-tag-search-handler'
};
}
}

View File

@ -0,0 +1,70 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreTagProvider } from './tag';
/**
* Handler to treat links to tag search.
*/
@Injectable()
export class CoreTagSearchLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreTagSearchLinkHandler';
pattern = /\/tag\/search\.php/;
constructor(private tagProvider: CoreTagProvider, private linkHelper: CoreContentLinksHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @param {any} [data] Extra data to handle the URL.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number, data?: any):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const pageParams = {
collectionId: parseInt(params.tc, 10) || 0,
query: params.query || '',
};
this.linkHelper.goInSite(navCtrl, 'CoreTagSearchPage', pageParams, siteId);
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
return this.tagProvider.areTagsAvailable(siteId);
}
}

View File

@ -0,0 +1,345 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
/**
* Structure of a tag cloud returned by WS.
*/
export interface CoreTagCloud {
tags: CoreTagCloudTag[];
tagscount: number;
totalcount: number;
}
/**
* Structure of a tag cloud tag returned by WS.
*/
export interface CoreTagCloudTag {
name: string;
viewurl: string;
flag: boolean;
isstandard: boolean;
count: number;
size: number;
}
/**
* Structure of a tag collection returned by WS.
*/
export interface CoreTagCollection {
id: number;
name: string;
isdefault: boolean;
component: string;
sortoder: number;
searchable: boolean;
customurl: string;
}
/**
* Structure of a tag index returned by WS.
*/
export interface CoreTagIndex {
tagid: number;
ta: number;
component: string;
itemtype: string;
nextpageurl: string;
prevpageurl: string;
exclusiveurl: string;
exclusivetext: string;
title: string;
content: string;
hascontent: number;
anchor: string;
}
/**
* Structure of a tag item returned by WS.
*/
export interface CoreTagItem {
id: number;
name: string;
rawname: string;
isstandard: boolean;
tagcollid: number;
taginstanceid: number;
taginstancecontextid: number;
itemid: number;
ordering: number;
flag: number;
}
/**
* Service to handle tags.
*/
@Injectable()
export class CoreTagProvider {
static SEARCH_LIMIT = 150;
protected ROOT_CACHE_KEY = 'CoreTag:';
constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {}
/**
* Check whether tags are available in a certain site.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if available, resolved with false otherwise.
* @since 3.7
*/
areTagsAvailable(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.areTagsAvailableInSite(site);
});
}
/**
* Check whether tags are available in a certain site.
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} True if available.
*/
areTagsAvailableInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.wsAvailable('core_tag_get_tagindex_per_area') &&
site.wsAvailable('core_tag_get_tag_cloud') &&
site.wsAvailable('core_tag_get_tag_collections') &&
!site.isFeatureDisabled('NoDelegate_CoreTag');
}
/**
* Fetch the tag cloud.
*
* @param {number} [collectionId=0] Tag collection ID.
* @param {boolean} [isStandard=false] Whether to return only standard tags.
* @param {string} [sort='name'] Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
* @param {string} [search=''] Search string.
* @param {number} [fromContextId=0] Context ID where this tag cloud is displayed.
* @param {number} [contextId=0] Only retrieve tag instances in this context.
* @param {boolean} [recursive=true] Retrieve tag instances in the context and its children.
* @param {number} [limit] Maximum number of tags to retrieve. Defaults to SEARCH_LIMIT.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<CoreTagCloud>} Promise resolved with the tag cloud.
* @since 3.7
*/
getTagCloud(collectionId: number = 0, isStandard: boolean = false, sort: string = 'name', search: string = '',
fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, limit?: number, siteId?: string):
Promise<CoreTagCloud> {
limit = limit || CoreTagProvider.SEARCH_LIMIT;
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
tagcollid: collectionId,
isstandard: isStandard,
limit: limit,
sort: sort,
search: search,
fromctx: fromContextId,
ctx: contextId,
rec: recursive
};
const preSets: CoreSiteWSPreSets = {
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
cacheKey: this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive),
getFromCache: search != '' // Try to get updated data when searching.
};
return site.read('core_tag_get_tag_cloud', params, preSets);
});
}
/**
* Fetch the tag collections.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<CoreTagCollection[]>} Promise resolved with the tag collections.
* @since 3.7
*/
getTagCollections(siteId?: string): Promise<CoreTagCollection[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const preSets: CoreSiteWSPreSets = {
updateFrequency: CoreSite.FREQUENCY_RARELY,
cacheKey: this.getTagCollectionsKey()
};
return site.read('core_tag_get_tag_collections', null, preSets).then((response) => {
if (!response || !response.collections) {
return Promise.reject(null);
}
return response.collections;
});
});
}
/**
* Fetch the tag index.
*
* @param {number} [id=0] Tag ID.
* @param {string} [name=''] Tag name.
* @param {number} [collectionId=0] Tag collection ID.
* @param {number} [areaId=0] Tag area ID.
* @param {number} [fromContextId=0] Context ID where the link was displayed.
* @param {number} [contextId=0] Context ID where to search for items.
* @param {boolean} [recursive=true] Search in the context and its children.
* @param {number} [page=0] Page number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<CoreTagIndex[]>} Promise resolved with the tag index per area.
* @since 3.7
*/
getTagIndexPerArea(id: number, name: string = '', collectionId: number = 0, areaId: number = 0, fromContextId: number = 0,
contextId: number = 0, recursive: boolean = true, page: number = 0, siteId?: string): Promise<CoreTagIndex[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
tagindex: {
id: id,
tag: name,
tc: collectionId,
ta: areaId,
excl: true,
from: fromContextId,
ctx: contextId,
rec: recursive,
page: page
},
};
const preSets: CoreSiteWSPreSets = {
updateFrequency: CoreSite.FREQUENCY_OFTEN,
cacheKey: this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive)
};
return site.read('core_tag_get_tagindex_per_area', params, preSets).catch((error) => {
// Workaround for WS not passing parameter to error string.
if (error && error.errorcode == 'notagsfound') {
error.message = this.translate.instant('core.tag.notagsfound', {$a: name || id || ''});
}
return Promise.reject(error);
}).then((response) => {
if (!response || !response.length) {
return Promise.reject(null);
}
return response;
});
});
}
/**
* Invalidate tag cloud.
*
* @param {number} [collectionId=0] Tag collection ID.
* @param {boolean} [isStandard=false] Whether to return only standard tags.
* @param {string} [sort='name'] Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
* @param {string} [search=''] Search string.
* @param {number} [fromContextId=0] Context ID where this tag cloud is displayed.
* @param {number} [contextId=0] Only retrieve tag instances in this context.
* @param {boolean} [recursive=true] Retrieve tag instances in the context and its children.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateTagCloud(collectionId: number = 0, isStandard: boolean = false, sort: string = 'name', search: string = '',
fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Invalidate tag collections.
*
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateTagCollections(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getTagCollectionsKey();
return site.invalidateWsCacheForKey(key);
});
}
/**
* Invalidate tag index.
*
* @param {number} [id=0] Tag ID.
* @param {string} [name=''] Tag name.
* @param {number} [collectionId=0] Tag collection ID.
* @param {number} [areaId=0] Tag area ID.
* @param {number} [fromContextId=0] Context ID where the link was displayed.
* @param {number} [contextId=0] Context ID where to search for items.
* @param {boolean} [recursive=true] Search in the context and its children.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateTagIndexPerArea(id: number, name: string = '', collectionId: number = 0, areaId: number = 0,
fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get cache key for tag cloud.
*
* @param {number} collectionId Tag collection ID.
* @param {boolean} isStandard Whether to return only standard tags.
* @param {string} sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
* @param {string} search Search string.
* @param {number} fromContextId Context ID where this tag cloud is displayed.
* @param {number} contextId Only retrieve tag instances in this context.
* @param {boolean} recursive Retrieve tag instances in the context and it's children.
* @return {string} Cache key.
*/
protected getTagCloudKey(collectionId: number, isStandard: boolean, sort: string, search: string, fromContextId: number,
contextId: number, recursive: boolean): string {
return this.ROOT_CACHE_KEY + 'cloud:' + collectionId + ':' + (isStandard ? 1 : 0) + ':' + sort + ':' + search + ':' +
fromContextId + ':' + contextId + ':' + (recursive ? 1 : 0);
}
/**
* Get cache key for tag collections.
*
* @return {string} Cache key.
*/
protected getTagCollectionsKey(): string {
return this.ROOT_CACHE_KEY + 'collections';
}
/**
* Get cache key for tag index.
*
* @param {number} id Tag ID.
* @param {string} name Tag name.
* @param {number} collectionId Tag collection ID.
* @param {number} areaId Tag area ID.
* @param {number} fromContextId Context ID where the link was displayed.
* @param {number} contextId Context ID where to search for items.
* @param {boolean} [recursive=true] Search in the context and its children.
* @return {string} Cache key.
*/
protected getTagIndexPerAreaKey(id: number, name: string, collectionId: number, areaId: number, fromContextId: number,
contextId: number, recursive: boolean): string {
return this.ROOT_CACHE_KEY + 'index:' + id + ':' + name + ':' + collectionId + ':' + areaId + ':' + fromContextId + ':'
+ contextId + ':' + (recursive ? 1 : 0);
}
}

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreTagProvider } from './providers/tag';
import { CoreTagHelperProvider } from './providers/helper';
import { CoreTagAreaDelegate } from './providers/area-delegate';
import { CoreTagMainMenuHandler } from './providers/mainmenu-handler';
import { CoreTagIndexLinkHandler } from './providers/index-link-handler';
import { CoreTagSearchLinkHandler } from './providers/search-link-handler';
@NgModule({
declarations: [
],
imports: [
],
providers: [
CoreTagProvider,
CoreTagHelperProvider,
CoreTagAreaDelegate,
CoreTagMainMenuHandler,
CoreTagIndexLinkHandler,
CoreTagSearchLinkHandler
]
})
export class CoreTagModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreTagMainMenuHandler,
contentLinksDelegate: CoreContentLinksDelegate, indexLinkHandler: CoreTagIndexLinkHandler,
searchLinkHandler: CoreTagSearchLinkHandler) {
mainMenuDelegate.registerHandler(mainMenuHandler);
contentLinksDelegate.registerHandler(indexLinkHandler);
contentLinksDelegate.registerHandler(searchLinkHandler);
}
}

View File

@ -18,6 +18,7 @@ import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CoreUserParticipantsComponent } from './participants/participants'; import { CoreUserParticipantsComponent } from './participants/participants';
import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field'; import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field';
import { CoreUserTagAreaComponent } from './tag-area/tag-area';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module'; import { CorePipesModule } from '@pipes/pipes.module';
@ -25,7 +26,8 @@ import { CorePipesModule } from '@pipes/pipes.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreUserParticipantsComponent, CoreUserParticipantsComponent,
CoreUserProfileFieldComponent CoreUserProfileFieldComponent,
CoreUserTagAreaComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -39,10 +41,12 @@ import { CorePipesModule } from '@pipes/pipes.module';
], ],
exports: [ exports: [
CoreUserParticipantsComponent, CoreUserParticipantsComponent,
CoreUserProfileFieldComponent CoreUserProfileFieldComponent,
CoreUserTagAreaComponent
], ],
entryComponents: [ entryComponents: [
CoreUserParticipantsComponent CoreUserParticipantsComponent,
CoreUserTagAreaComponent
] ]
}) })
export class CoreUserComponentsModule {} export class CoreUserComponentsModule {}

View File

@ -0,0 +1,4 @@
<a ion-item text-wrap *ngFor="let item of items" core-user-link [userId]="item.id">
<ion-avatar core-user-avatar [user]="item" item-start></ion-avatar>
<h2>{{ item.fullname }}</h2>
</a>

View File

@ -0,0 +1,26 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
/**
* Component to render the user tag area.
*/
@Component({
selector: 'core-user-tag-area',
templateUrl: 'core-user-tag-area.html'
})
export class CoreUserTagAreaComponent {
@Input() items: any[]; // Area items to render.
}

View File

@ -0,0 +1,82 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable, Injector } from '@angular/core';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
import { CoreUserTagAreaComponent } from '../components/tag-area/tag-area';
/**
* Handler to support tags.
*/
@Injectable()
export class CoreUserTagAreaHandler implements CoreTagAreaHandler {
name = 'CoreUserTagAreaHandler';
type = 'core/user';
constructor(private domUtils: CoreDomUtilsProvider) {}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param {string} content Rendered content.
* @return {any[]|Promise<any[]>} Area items (or promise resolved with the items).
*/
parseContent(content: string): any[] | Promise<any[]> {
const items = [];
const element = this.domUtils.convertToElement(content);
Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => {
const item: any = {};
const avatarLink = userbox.querySelector('a:first-child');
if (!avatarLink) {
return;
}
const profileUrl = avatarLink.getAttribute('href') || '';
const match = profileUrl.match(/.*\/user\/(?:profile|view)\.php\?id=(\d+)/);
if (!match) {
return;
}
item.id = parseInt(match[1], 10);
const avatarImg = avatarLink.querySelector('img.userpicture');
item.profileimageurl = avatarImg ? avatarImg.getAttribute('src') : '';
item.fullname = userbox.innerText;
items.push(item);
});
return items;
}
/**
* Get the component to use to display items.
*
* @param {Injector} injector Injector.
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(injector: Injector): any | Promise<any> {
return CoreUserTagAreaComponent;
}
}

View File

@ -30,6 +30,8 @@ import { CoreCronDelegate } from '@providers/cron';
import { CoreUserOfflineProvider } from './providers/offline'; import { CoreUserOfflineProvider } from './providers/offline';
import { CoreUserSyncProvider } from './providers/sync'; import { CoreUserSyncProvider } from './providers/sync';
import { CoreUserSyncCronHandler } from './providers/sync-cron-handler'; import { CoreUserSyncCronHandler } from './providers/sync-cron-handler';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { CoreUserTagAreaHandler } from './providers/tag-area-handler';
// List of providers (without handlers). // List of providers (without handlers).
export const CORE_USER_PROVIDERS: any[] = [ export const CORE_USER_PROVIDERS: any[] = [
@ -59,6 +61,7 @@ export const CORE_USER_PROVIDERS: any[] = [
CoreUserParticipantsCourseOptionHandler, CoreUserParticipantsCourseOptionHandler,
CoreUserParticipantsLinkHandler, CoreUserParticipantsLinkHandler,
CoreUserSyncCronHandler, CoreUserSyncCronHandler,
CoreUserTagAreaHandler
] ]
}) })
export class CoreUserModule { export class CoreUserModule {
@ -67,13 +70,14 @@ export class CoreUserModule {
contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler, contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler,
courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler, courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler,
courseOptionsDelegate: CoreCourseOptionsDelegate, cronDelegate: CoreCronDelegate, courseOptionsDelegate: CoreCourseOptionsDelegate, cronDelegate: CoreCronDelegate,
syncHandler: CoreUserSyncCronHandler) { syncHandler: CoreUserSyncCronHandler, tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: CoreUserTagAreaHandler) {
userDelegate.registerHandler(userProfileMailHandler); userDelegate.registerHandler(userProfileMailHandler);
courseOptionsDelegate.registerHandler(courseOptionHandler); courseOptionsDelegate.registerHandler(courseOptionHandler);
contentLinksDelegate.registerHandler(userLinkHandler); contentLinksDelegate.registerHandler(userLinkHandler);
contentLinksDelegate.registerHandler(linkHandler); contentLinksDelegate.registerHandler(linkHandler);
cronDelegate.register(syncHandler); cronDelegate.register(syncHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => { eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => {
// Search for userid in params. // Search for userid in params.

View File

@ -394,7 +394,7 @@ export class CoreFormatTextDirective implements OnChanges {
anchors.forEach((anchor) => { anchors.forEach((anchor) => {
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
const linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils, const linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils,
this.contentLinksHelper, this.navCtrl, this.content, this.svComponent); this.contentLinksHelper, this.navCtrl, this.content, this.svComponent, this.textUtils);
linkDir.capture = true; linkDir.capture = true;
linkDir.ngOnInit(); linkDir.ngOnInit();

View File

@ -21,6 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreConfigConstants } from '../configconstants'; import { CoreConfigConstants } from '../configconstants';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreTextUtilsProvider } from '@providers/utils/text';
/** /**
* Directive to open a link in external browser. * Directive to open a link in external browser.
@ -41,7 +42,8 @@ export class CoreLinkDirective implements OnInit {
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider,
private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController,
@Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent) { @Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent,
private textUtils: CoreTextUtilsProvider) {
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
this.element = element.nativeElement || element; this.element = element.nativeElement || element;
} }
@ -62,12 +64,13 @@ export class CoreLinkDirective implements OnInit {
this.element.addEventListener('click', (event) => { this.element.addEventListener('click', (event) => {
// If the event prevented default action, do nothing. // If the event prevented default action, do nothing.
if (!event.defaultPrevented) { if (!event.defaultPrevented) {
const href = this.element.getAttribute('href'); let href = this.element.getAttribute('href');
if (href) { if (href) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (this.utils.isTrueOrOne(this.capture)) { if (this.utils.isTrueOrOne(this.capture)) {
href = this.textUtils.decodeURI(href);
this.contentLinksHelper.handleLink(href, undefined, navCtrl, true, true).then((treated) => { this.contentLinksHelper.handleLink(href, undefined, navCtrl, true, true).then((treated) => {
if (!treated) { if (!treated) {
this.navigate(href); this.navigate(href);