diff --git a/scripts/langindex.json b/scripts/langindex.json index 679e4d559..10ac601e4 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1800,8 +1800,13 @@ "core.submit": "moodle", "core.success": "moodle", "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", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 69730eab9..726ea14f2 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1800,8 +1800,13 @@ "core.submit": "Submit", "core.success": "Success", "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", diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index b7f8b8794..c23afc5e9 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -1,6 +1,11 @@ { + "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", diff --git a/src/core/tag/pages/search/search.html b/src/core/tag/pages/search/search.html new file mode 100644 index 000000000..5635d09d9 --- /dev/null +++ b/src/core/tag/pages/search/search.html @@ -0,0 +1,37 @@ + + + {{ 'core.tag.searchtags' | translate }} + + + + + + + + + 1 ? '' : null"> + + + 1"> + + {{ 'core.tag.inalltagcoll' | translate }} + {{ collection.name }} + + + + + + + + 0"> + + + {{ tag.name }} + + + + {{ 'core.tag.showingfirsttags' | translate: {$a: cloud.tags.length} }} + + + + diff --git a/src/core/tag/pages/search/search.module.ts b/src/core/tag/pages/search/search.module.ts new file mode 100644 index 000000000..29776ce68 --- /dev/null +++ b/src/core/tag/pages/search/search.module.ts @@ -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 {} diff --git a/src/core/tag/pages/search/search.scss b/src/core/tag/pages/search/search.scss new file mode 100644 index 000000000..cd7173445 --- /dev/null +++ b/src/core/tag/pages/search/search.scss @@ -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; + } + } +} diff --git a/src/core/tag/pages/search/search.ts b/src/core/tag/pages/search/search.ts new file mode 100644 index 000000000..13f09bb4c --- /dev/null +++ b/src/core/tag/pages/search/search.ts @@ -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 { + return Promise.all([ + this.fetchCollections(), + this.fetchTags() + ]).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading tags.'); + }); + } + + /** + * Fetch tag collections. + * + * @return {Promise} Resolved when done. + */ + fetchCollections(): Promise { + 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} Resolved when done. + */ + fetchTags(): Promise { + 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} Resolved when done. + */ + searchTags(query: string): Promise { + 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; + }); + } +} diff --git a/src/core/tag/providers/mainmenu-handler.ts b/src/core/tag/providers/mainmenu-handler.ts new file mode 100644 index 000000000..8e676acc1 --- /dev/null +++ b/src/core/tag/providers/mainmenu-handler.ts @@ -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} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + 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' + }; + } +} diff --git a/src/core/tag/providers/tag.ts b/src/core/tag/providers/tag.ts index 88b739f04..3a377cba9 100644 --- a/src/core/tag/providers/tag.ts +++ b/src/core/tag/providers/tag.ts @@ -17,6 +17,40 @@ 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. */ @@ -57,6 +91,8 @@ export interface CoreTagItem { @Injectable() export class CoreTagProvider { + static SEARCH_LIMIT = 150; + protected ROOT_CACHE_KEY = 'CoreTag:'; constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {} @@ -89,6 +125,71 @@ export class CoreTagProvider { !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} 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 { + 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} Promise resolved with the tag collections. + * @since 3.7 + */ + getTagCollections(siteId?: string): Promise { + 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. * @@ -142,6 +243,40 @@ export class CoreTagProvider { }); } + /** + * 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} 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 { + 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} Promise resolved when the data is invalidated. + */ + invalidateTagCollections(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getTagCollectionsKey(); + + return site.invalidateWsCacheForKey(key); + }); + } + /** * Invalidate tag index. * @@ -163,6 +298,33 @@ export class CoreTagProvider { }); } + /** + * 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. * diff --git a/src/core/tag/tag.module.ts b/src/core/tag/tag.module.ts index 45d4d69af..baa55d98f 100644 --- a/src/core/tag/tag.module.ts +++ b/src/core/tag/tag.module.ts @@ -13,9 +13,11 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreTagProvider } from './providers/tag'; import { CoreTagHelperProvider } from './providers/helper'; import { CoreTagAreaDelegate } from './providers/area-delegate'; +import { CoreTagMainMenuHandler } from './providers/mainmenu-handler'; @NgModule({ declarations: [ @@ -25,8 +27,13 @@ import { CoreTagAreaDelegate } from './providers/area-delegate'; providers: [ CoreTagProvider, CoreTagHelperProvider, - CoreTagAreaDelegate + CoreTagAreaDelegate, + CoreTagMainMenuHandler ] }) export class CoreTagModule { + + constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreTagMainMenuHandler) { + mainMenuDelegate.registerHandler(mainMenuHandler); + } }
+ {{ 'core.tag.showingfirsttags' | translate: {$a: cloud.tags.length} }} +