diff --git a/.gitignore b/.gitignore index 4d221f5fa..0bca4e881 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ Thumbs.db /src/assets/lib /src/assets/lang/* /src/assets/env.json +/src/assets/fonts/icons.json /moodle.config.*.json !/moodle.config.example.json diff --git a/gulp/task-build-icons-json.js b/gulp/task-build-icons-json.js new file mode 100644 index 000000000..2d2717f8b --- /dev/null +++ b/gulp/task-build-icons-json.js @@ -0,0 +1,118 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const { writeFile, readdirSync, statSync, readFileSync } = require('fs'); + +const FONTS_PATH = 'src/assets/fonts'; +const ICONS_JSON_FILE_PATH = 'src/assets/fonts/icons.json'; + +/** + * Get object with the map of icons for all fonts. + * + * @returns Icons map. + */ +function getIconsMap() { + const config = JSON.parse(readFileSync('moodle.config.json')); + let icons = {}; + + const fonts = readdirSync(FONTS_PATH); + fonts.forEach(font => { + const fontPath = `${FONTS_PATH}/${font}`; + if (statSync(fontPath).isFile()) { + // Not a font, ignore. + return; + } + + icons = { + ...icons, + ...getFontIconsMap(config.iconsPrefixes, font, fontPath), + }; + }); + + return icons; +} + +/** + * Get object with the map of icons for a certain font. + * + * @param prefixes Prefixes to add to the icons. + * @param fontName Font name. + * @param fontPath Font path. + * @returns Icons map. + */ +function getFontIconsMap(prefixes, fontName, fontPath) { + const icons = {}; + const fontLibraries = readdirSync(fontPath); + + fontLibraries.forEach(libraryName => { + const libraryPath = `${fontPath}/${libraryName}`; + if (statSync(libraryPath).isFile()) { + // Not a font library, ignore. + return; + } + + const libraryPrefixes = prefixes?.[fontName]?.[libraryName]; + if (!libraryPrefixes || !libraryPrefixes.length) { + console.warn(`WARNING: There is no prefix for the library ${fontName}/${libraryName}. Adding icons without prefix is ` + + 'discouraged, please add a prefix for your library in moodle.config.json file, in the iconsPrefixes property.'); + } + + const libraryIcons = readdirSync(libraryPath); + libraryIcons.forEach(iconFileName => { + if (!iconFileName.endsWith('.svg')) { + // Only accept svg files. + return; + } + + if (iconFileName.includes('_')) { + throw Error(`Icon ${libraryPath}/${iconFileName} has an invalid name, it cannot contain '_'. ` + + 'Please rename it to use \'-\' instead.'); + } + + const iconName = iconFileName.replace('.svg', ''); + const iconPath = `${libraryPath}/${iconFileName}`.replace('src/', ''); + + if (!libraryPrefixes || !libraryPrefixes.length) { + icons[iconName] = iconPath; + return; + } + + libraryPrefixes.forEach(prefix => { + icons[`${prefix}-${iconName}`] = iconPath; + }); + }); + }); + + return icons; +} + +/** + * Task to build a JSON file with the list of icons to add to Ionicons. + */ +class BuildIconsJsonTask { + + /** + * Run the task. + * + * @param done Function to call when done. + */ + run(done) { + const icons = getIconsMap(); + + writeFile(ICONS_JSON_FILE_PATH, JSON.stringify(icons), done); + } + +} + +module.exports = BuildIconsJsonTask; diff --git a/gulpfile.js b/gulpfile.js index e7f127210..75ec00a39 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,6 +15,7 @@ const BuildLangTask = require('./gulp/task-build-lang'); const BuildBehatPluginTask = require('./gulp/task-build-behat-plugin'); const BuildEnvTask = require('./gulp/task-build-env'); +const BuildIconsJsonTask = require('./gulp/task-build-icons-json'); const PushTask = require('./gulp/task-push'); const OverrideLangTask = require('./gulp/task-override-lang'); const Utils = require('./gulp/utils'); @@ -47,6 +48,10 @@ gulp.task('env', (done) => { new BuildEnvTask().run(done); }); +gulp.task('icons', (done) => { + new BuildIconsJsonTask().run(done); +}); + // Build a Moodle plugin to run Behat tests. if (BuildBehatPluginTask.isBehatConfigured()) { gulp.task('behat', (done) => { @@ -63,6 +68,7 @@ gulp.task( gulp.parallel([ 'lang', 'env', + 'icons', ...(BuildBehatPluginTask.isBehatConfigured() ? ['behat'] : []) ]), ); diff --git a/moodle.config.json b/moodle.config.json index dbf81aa21..33aeb6439 100644 --- a/moodle.config.json +++ b/moodle.config.json @@ -111,5 +111,16 @@ "long": 3500, "sticky": 0 }, - "disableTokenFile": false + "disableTokenFile": false, + "iconsPrefixes": { + "font-awesome": { + "brands": ["fab"], + "regular": ["far"], + "solid": ["fa", "fas"] + }, + "moodle": { + "font-awesome": ["fam"], + "moodle": ["moodle"] + } + } } diff --git a/src/assets/fonts/font-awesome/aliases.json b/src/assets/fonts/font-awesome/aliases.json index 62d54e2d0..ba5a083a4 100644 --- a/src/assets/fonts/font-awesome/aliases.json +++ b/src/assets/fonts/font-awesome/aliases.json @@ -1,6 +1,8 @@ { "ad": "rectangle-ad", "add": "plus", + "address-book-o": "address-book", + "address-card-o": "address-card", "adjust": "circle-half-stroke", "air-freshener": "spray-can-sparkles", "allergies": "hand-dots", @@ -30,34 +32,37 @@ "arrow-rotate-backward": "arrow-rotate-left", "arrow-rotate-forward": "arrow-rotate-right", "arrow-turn-right": "share", + "arrows": "arrows-up-down-left-right", + "arrows-alt": "up-down-left-right", "arrows-alt-h": "left-right", "arrows-alt-v": "up-down", - "arrows-alt": "up-down-left-right", "arrows-h": "arrows-left-right", "arrows-v": "arrows-up-down", - "arrows": "arrows-up-down-left-right", "asl-interpreting": "hands-asl-interpreting", "assistive-listening-systems": "ear-listen", "atlas": "book-atlas", "automobile": "car", "backspace": "delete-left", + "balance-scale": "scale-balanced", "balance-scale-left": "scale-unbalanced", "balance-scale-right": "scale-unbalanced-flip", - "balance-scale": "scale-balanced", "band-aid": "bandage", "bank": "building-columns", "bar-chart": "chart-bar", + "bar-chart-o": "chart-column", "baseball-ball": "baseball", "basketball-ball": "basketball", "bathtub": "bath", + "battery": "battery-full", "battery-0": "battery-empty", "battery-2": "battery-quarter", "battery-3": "battery-half", "battery-4": "battery-three-quarters", "battery-5": "battery-full", "battery-car": "car-battery", - "battery": "battery-full", "beer": "beer-mug-empty", + "bell-o": "bell", + "bell-slash-o": "bell-slash", "bible": "book-bible", "biking": "person-biking", "birthday-cake": "cake-candles", @@ -65,17 +70,24 @@ "blind": "person-walking-with-cane", "book-dead": "book-skull", "book-reader": "book-open-reader", + "bookmark-o": "bookmark", "border-style": "border-top-left", - "boxes-alt": "boxes-stacked", "boxes": "boxes-stacked", + "boxes-alt": "boxes-stacked", "briefcase-clock": "business-time", "broadcast-tower": "tower-broadcast", + "building-o": "building", "burn": "fire-flame-simple", "bus-alt": "bus-simple", "cab": "taxi", "cake": "cake-candles", "calendar-alt": "calendar-days", + "calendar-check-o": "calendar-check", + "calendar-minus-o": "calendar-minus", + "calendar-o": "calendar", + "calendar-plus-o": "calendar-plus", "calendar-times": "calendar-xmark", + "calendar-times-o": "calendar-xmark", "camera-alt": "camera", "cancel": "ban", "car-alt": "car-rear", @@ -85,24 +97,28 @@ "caret-square-right": "square-caret-right", "caret-square-up": "square-caret-up", "carriage-baby": "baby-carriage", + "chain": "link", "chain-broken": "link-slash", "chain-slash": "link-slash", - "chain": "link", "chalkboard-teacher": "chalkboard-user", "check-circle": "circle-check", + "check-circle-o": "circle-check", "check-square": "square-check", + "check-square-o": "square-check", "chevron-circle-down": "circle-chevron-down", "chevron-circle-left": "circle-chevron-left", "chevron-circle-right": "circle-chevron-right", "chevron-circle-up": "circle-chevron-up", "child-rifle": "child-combatant", + "circle-o": "circle", "clinic-medical": "house-chimney-medical", "clock-four": "clock", + "clock-o": "clock", "close": "xmark", - "cloud-download-alt": "cloud-arrow-down", "cloud-download": "cloud-arrow-down", - "cloud-upload-alt": "cloud-arrow-up", + "cloud-download-alt": "cloud-arrow-down", "cloud-upload": "cloud-arrow-up", + "cloud-upload-alt": "cloud-arrow-up", "cny": "yen-sign", "cocktail": "martini-glass-citrus", "coffee": "mug-saucer", @@ -110,7 +126,10 @@ "cogs": "gears", "columns": "table-columns", "comment-alt": "message", + "comment-o": "comment", "commenting": "comment-dots", + "commenting-o": "comment-dots", + "comments-o": "comments", "compress-alt": "down-left-and-up-right-to-center", "compress-arrows-alt": "minimize", "concierge-bell": "bell-concierge", @@ -135,26 +154,30 @@ "dolly-flatbed": "cart-flatbed", "donate": "circle-dollar-to-slot", "dot-circle": "circle-dot", + "dot-circle-o": "circle-dot", "drafting-compass": "compass-drafting", "drivers-license": "id-card", - "earth-america": "earth-americas", + "drivers-license-o": "id-card", "earth": "earth-americas", + "earth-america": "earth-americas", "edit": "pen", "ellipsis-h": "ellipsis", "ellipsis-v": "ellipsis-vertical", + "envelope-o": "envelope", + "envelope-open-o": "envelope-open", "envelope-square": "square-envelope", "eur": "euro-sign", "euro": "euro-sign", - "exchange-alt": "right-left", "exchange": "arrow-right-arrow-left", + "exchange-alt": "right-left", "exclamation-circle": "circle-exclamation", "exclamation-triangle": "triangle-exclamation", "expand-alt": "up-right-and-down-left-from-center", "expand-arrows-alt": "maximize", - "external-link-alt": "up-right-from-square", - "external-link-square-alt": "square-up-right", - "external-link-square": "square-arrow-up-right", "external-link": "arrow-up-right-from-square", + "external-link-alt": "up-right-from-square", + "external-link-square": "square-arrow-up-right", + "external-link-square-alt": "square-up-right", "eye-dropper-empty": "eye-dropper", "eyedropper": "eye-dropper", "facebook-square": "square-facebook", @@ -166,28 +189,51 @@ "fighter-jet": "jet-fighter", "file-alt": "file-lines", "file-archive": "file-zipper", + "file-archive-o": "file-zipper", + "file-audio-o": "file-audio", "file-clipboard": "paste", + "file-code-o": "file-code", "file-download": "file-arrow-down", "file-edit": "file-pen", + "file-excel-o": "file-excel", + "file-image-o": "file-image", "file-medical-alt": "file-waveform", + "file-movie-o": "file-video", + "file-o": "file", + "file-pdf-o": "file-pdf", + "file-photo-o": "file-image", + "file-picture-o": "file-image", + "file-powerpoint-o": "file-powerpoint", + "file-sound-o": "file-audio", "file-text": "file-lines", + "file-text-o": "file-lines", "file-upload": "file-arrow-up", + "file-video-o": "file-video", + "file-word-o": "file-word", + "file-zip-o": "file-zipper", + "files-o": "copy", "fire-alt": "fire-flame-curved", "first-aid": "kit-medical", "fist-raised": "hand-fist", + "flag-o": "flag", + "floppy-o": "floppy-disk", "flushed": "face-flushed", "folder-blank": "folder", + "folder-o": "folder", + "folder-open-o": "folder-open", "football-ball": "football", - "frown-open": "face-frown-open", "frown": "face-frown", + "frown-o": "face-frown", + "frown-open": "face-frown-open", "funnel-dollar": "filter-circle-dollar", "futbol-ball": "futbol", + "futbol-o": "futbol", "gauge-med": "gauge", "gauge-simple-med": "gauge-simple", "gbp": "sterling-sign", "glass-cheers": "champagne-glasses", - "glass-martini-alt": "martini-glass", "glass-martini": "martini-glass-empty", + "glass-martini-alt": "martini-glass", "glass-whiskey": "whiskey-glass", "globe-africa": "earth-africa", "globe-americas": "earth-americas", @@ -196,49 +242,62 @@ "globe-oceania": "earth-oceania", "golf-ball": "golf-ball-tee", "grimace": "face-grimace", + "grin": "face-grin", "grin-alt": "face-grin-wide", - "grin-beam-sweat": "face-grin-beam-sweat", "grin-beam": "face-grin-beam", + "grin-beam-sweat": "face-grin-beam-sweat", "grin-hearts": "face-grin-hearts", - "grin-squint-tears": "face-grin-squint-tears", "grin-squint": "face-grin-squint", + "grin-squint-tears": "face-grin-squint-tears", "grin-stars": "face-grin-stars", "grin-tears": "face-grin-tears", + "grin-tongue": "face-grin-tongue", "grin-tongue-squint": "face-grin-tongue-squint", "grin-tongue-wink": "face-grin-tongue-wink", - "grin-tongue": "face-grin-tongue", "grin-wink": "face-grin-wink", - "grin": "face-grin", "grip-horizontal": "grip", "h-square": "square-h", "hamburger": "burger", + "hand-grab-o": "hand-back-fist", "hand-holding-usd": "hand-holding-dollar", "hand-holding-water": "hand-holding-droplet", + "hand-lizard-o": "hand-lizard", "hand-paper": "hand", + "hand-paper-o": "hand", + "hand-peace-o": "hand-peace", + "hand-pointer-o": "hand-pointer", "hand-rock": "hand-back-fist", + "hand-rock-o": "hand-back-fist", + "hand-scissors-o": "hand-scissors", + "hand-spock-o": "hand-spock", + "hand-stop-o": "hand", "hands-american-sign-language-interpreting": "hands-asl-interpreting", "hands-helping": "handshake-angle", "hands-wash": "hands-bubbles", - "handshake-alt-slash": "handshake-simple-slash", "handshake-alt": "handshake-simple", + "handshake-alt-slash": "handshake-simple-slash", + "handshake-o": "handshake", "hard-hat": "helmet-safety", "hard-of-hearing": "ear-deaf", "hat-hard": "helmet-safety", "haykal": "bahai", "hdd": "hard-drive", + "hdd-o": "hard-drive", "header": "heading", "headphones-alt": "headphones-simple", "heart-broken": "heart-crack", "heart-music-camera-bolt": "icons", + "heart-o": "heart", "heartbeat": "heart-pulse", "hiking": "person-hiking", "history": "clock-rotate-left", - "home-alt": "house", - "home-lg-alt": "house", - "home-lg": "house-chimney", - "home-user": "house-user", "home": "house", + "home-alt": "house", + "home-lg": "house-chimney", + "home-lg-alt": "house", + "home-user": "house-user", "hospital-alt": "hospital", + "hospital-o": "hospital", "hospital-symbol": "circle-h", "hospital-wide": "hospital", "hot-tub": "hot-tub-person", @@ -246,9 +305,11 @@ "hourglass-2": "hourglass-half", "hourglass-3": "hourglass-end", "hourglass-empty": "hourglass", + "hourglass-o": "hourglass", "house-damage": "house-chimney-crack", "hryvnia": "hryvnia-sign", "id-card-alt": "id-card-clip", + "id-card-o": "id-card", "ils": "shekel-sign", "indian-rupee": "indian-rupee-sign", "info-circle": "circle-info", @@ -256,22 +317,25 @@ "institution": "building-columns", "journal-whills": "book-journal-whills", "jpy": "yen-sign", + "keyboard-o": "keyboard", + "kiss": "face-kiss", "kiss-beam": "face-kiss-beam", "kiss-wink-heart": "face-kiss-wink-heart", - "kiss": "face-kiss", "krw": "won-sign", "ladder-water": "water-ladder", "landmark-alt": "landmark-dome", "laptop-house": "house-laptop", + "laugh": "face-laugh", "laugh-beam": "face-laugh-beam", "laugh-squint": "face-laugh-squint", "laugh-wink": "face-laugh-wink", - "laugh": "face-laugh", "legal": "gavel", - "level-down-alt": "turn-down", + "lemon-o": "lemon", "level-down": "arrow-turn-down", - "level-up-alt": "turn-up", + "level-down-alt": "turn-down", "level-up": "arrow-turn-up", + "level-up-alt": "turn-up", + "lightbulb-o": "lightbulb", "line-chart": "chart-line", "list-1-2": "list-ol", "list-alt": "rectangle-list", @@ -289,64 +353,75 @@ "long-arrow-up": "arrow-up-long", "low-vision": "eye-low-vision", "luggage-cart": "cart-flatbed-suitcase", - "magic-wand-sparkles": "wand-magic-sparkles", "magic": "wand-magic", + "magic-wand-sparkles": "wand-magic-sparkles", "mail-bulk": "envelopes-bulk", "mail-forward": "share", - "mail-reply-all": "reply-all", "mail-reply": "reply", + "mail-reply-all": "reply-all", "male": "person", - "map-marked-alt": "map-location-dot", "map-marked": "map-location", - "map-marker-alt": "location-dot", + "map-marked-alt": "map-location-dot", "map-marker": "location-pin", + "map-marker-alt": "location-dot", + "map-o": "map", "map-signs": "signs-post", "mars-stroke-h": "mars-stroke-right", "mars-stroke-v": "mars-stroke-up", "medkit": "suitcase-medical", - "meh-blank": "face-meh-blank", - "meh-rolling-eyes": "face-rolling-eyes", "meh": "face-meh", - "microphone-alt-slash": "microphone-lines-slash", + "meh-blank": "face-meh-blank", + "meh-o": "face-meh", + "meh-rolling-eyes": "face-rolling-eyes", "microphone-alt": "microphone-lines", + "microphone-alt-slash": "microphone-lines-slash", "minus-circle": "circle-minus", "minus-square": "square-minus", + "minus-square-o": "square-minus", "mobile-alt": "mobile-screen-button", - "mobile-android-alt": "mobile-screen", "mobile-android": "mobile", + "mobile-android-alt": "mobile-screen", "mobile-phone": "mobile", "money-bill-alt": "money-bill-1", "money-bill-wave-alt": "money-bill-1-wave", "money-check-alt": "money-check-dollar", + "moon-o": "moon", "mortar-board": "graduation-cap", - "mouse-pointer": "arrow-pointer", "mouse": "computer-mouse", + "mouse-pointer": "arrow-pointer", "multiply": "xmark", "museum": "building-columns", "navicon": "bars", + "newspaper-o": "newspaper", "paint-brush": "paintbrush", + "paper-plane-o": "paper-plane", "parking": "square-parking", "pastafarianism": "spaghetti-monster-flying", "pause-circle": "circle-pause", + "pause-circle-o": "circle-pause", "pen-alt": "pen-clip", "pen-square": "square-pen", "pencil-alt": "pencil", "pencil-ruler": "pen-ruler", "pencil-square": "square-pen", + "pencil-square-o": "pen-to-square", "people-arrows-left-right": "people-arrows", "people-carry": "people-carry-box", "percentage": "percent", "phone-alt": "phone-flip", - "phone-square-alt": "square-phone-flip", "phone-square": "square-phone", + "phone-square-alt": "square-phone-flip", "photo-video": "photo-film", + "picture-o": "image", "pie-chart": "chart-pie", "ping-pong-paddle-ball": "table-tennis-paddle-ball", "play-circle": "circle-play", + "play-circle-o": "circle-play", "plus-circle": "circle-plus", "plus-square": "square-plus", - "poll-h": "square-poll-horizontal", + "plus-square-o": "square-plus", "poll": "square-poll-vertical", + "poll-h": "square-poll-horizontal", "poo-bolt": "poo-storm", "portrait": "image-portrait", "pound-sign": "sterling-sign", @@ -356,19 +431,20 @@ "procedures": "bed-pulse", "project-diagram": "diagram-project", "question-circle": "circle-question", - "quidditch-broom-ball": "broom-ball", + "question-circle-o": "circle-question", "quidditch": "broom-ball", + "quidditch-broom-ball": "broom-ball", "quote-left-alt": "quote-left", "quote-right-alt": "quote-right", "quran": "book-quran", "radiation-alt": "circle-radiation", "random": "shuffle", "rectangle-times": "rectangle-xmark", - "redo-alt": "rotate-right", "redo": "arrow-rotate-right", + "redo-alt": "rotate-right", "refresh": "arrows-rotate", - "remove-format": "text-slash", "remove": "xmark", + "remove-format": "text-slash", "reorder": "bars-staggered", "rmb": "yen-sign", "rod-asclepius": "staff-snake", @@ -385,17 +461,19 @@ "sad-cry": "face-sad-cry", "sad-tear": "face-sad-tear", "save": "floppy-disk", + "search": "magnifying-glass", "search-dollar": "magnifying-glass-dollar", "search-location": "magnifying-glass-location", "search-minus": "magnifying-glass-minus", "search-plus": "magnifying-glass-plus", - "search": "magnifying-glass", - "share-alt-square": "square-share-nodes", + "send-o": "paper-plane", "share-alt": "share-nodes", + "share-alt-square": "square-share-nodes", "share-square": "share-from-square", + "share-square-o": "share-from-square", "shekel": "shekel-sign", - "sheqel-sign": "shekel-sign", "sheqel": "shekel-sign", + "sheqel-sign": "shekel-sign", "shield-alt": "shield-halved", "shield-blank": "shield", "shipping-fast": "truck-fast", @@ -403,78 +481,87 @@ "shopping-basket": "basket-shopping", "shopping-cart": "cart-shopping", "shuttle-van": "van-shuttle", - "sign-in-alt": "right-to-bracket", - "sign-in": "arrow-right-to-bracket", - "sign-language": "hands", - "sign-out-alt": "right-from-bracket", - "sign-out": "arrow-right-from-bracket", "sign": "sign-hanging", + "sign-in": "arrow-right-to-bracket", + "sign-in-alt": "right-to-bracket", + "sign-language": "hands", + "sign-out": "arrow-right-from-bracket", + "sign-out-alt": "right-from-bracket", "signal-5": "signal", "signal-perfect": "signal", "signing": "hands", "skating": "person-skating", - "skiing-nordic": "person-skiing-nordic", "skiing": "person-skiing", + "skiing-nordic": "person-skiing-nordic", "sliders-h": "sliders", - "smile-beam": "face-smile-beam", - "smile-wink": "face-smile-wink", "smile": "face-smile", + "smile-beam": "face-smile-beam", + "smile-o": "face-smile", + "smile-wink": "face-smile-wink", "smoking-ban": "ban-smoking", "sms": "comment-sms", "snowboarding": "person-snowboarding", + "snowflake-o": "snowflake", "soccer-ball": "futbol", + "soccer-ball-o": "futbol", "sort-alpha-asc": "arrow-down-a-z", "sort-alpha-desc": "arrow-down-z-a", - "sort-alpha-down-alt": "arrow-down-z-a", "sort-alpha-down": "arrow-down-a-z", - "sort-alpha-up-alt": "arrow-up-z-a", + "sort-alpha-down-alt": "arrow-down-z-a", "sort-alpha-up": "arrow-up-a-z", + "sort-alpha-up-alt": "arrow-up-z-a", "sort-amount-asc": "arrow-down-wide-short", "sort-amount-desc": "arrow-down-short-wide", - "sort-amount-down-alt": "arrow-down-short-wide", "sort-amount-down": "arrow-down-wide-short", - "sort-amount-up-alt": "arrow-up-short-wide", + "sort-amount-down-alt": "arrow-down-short-wide", "sort-amount-up": "arrow-up-wide-short", + "sort-amount-up-alt": "arrow-up-short-wide", "sort-asc": "sort-up", "sort-desc": "sort-down", "sort-numeric-asc": "arrow-down-1-9", "sort-numeric-desc": "arrow-down-9-1", - "sort-numeric-down-alt": "arrow-down-9-1", "sort-numeric-down": "arrow-down-1-9", - "sort-numeric-up-alt": "arrow-up-9-1", + "sort-numeric-down-alt": "arrow-down-9-1", "sort-numeric-up": "arrow-up-1-9", + "sort-numeric-up-alt": "arrow-up-9-1", "space-shuttle": "shuttle-space", "sprout": "seedling", + "square-o": "square", "square-root-alt": "square-root-variable", "staff-aesculapius": "staff-snake", "star-half-alt": "star-half-stroke", + "star-half-o": "star-half-stroke", + "star-o": "star", "step-backward": "backward-step", "step-forward": "forward-step", "sticky-note": "note-sticky", + "sticky-note-o": "note-sticky", "stop-circle": "circle-stop", - "store-alt-slash": "shop-slash", + "stop-circle-o": "circle-stop", "store-alt": "shop", + "store-alt-slash": "shop-slash", "stream": "bars-staggered", "subtract": "minus", "subway": "train-subway", + "sun-o": "sun", "surprise": "face-surprise", "swimmer": "person-swimming", "swimming-pool": "water-ladder", - "sync-alt": "rotate", "sync": "arrows-rotate", + "sync-alt": "rotate", "t-shirt": "shirt", "table-tennis": "table-tennis-paddle-ball", "tablet-alt": "tablet-screen-button", "tablet-android": "tablet", + "tachometer": "gauge-simple-high", + "tachometer-alt": "gauge-high", "tachometer-alt-average": "gauge", "tachometer-alt-fast": "gauge-high", - "tachometer-alt": "gauge-high", "tachometer-average": "gauge-simple", "tachometer-fast": "gauge-simple-high", - "tachometer": "gauge-simple-high", "tanakh": "book-tanakh", - "tasks-alt": "bars-progress", "tasks": "list-check", + "tasks-alt": "bars-progress", "teletype": "tty", "television": "tv", "temperature-0": "temperature-empty", @@ -485,9 +572,9 @@ "temperature-down": "temperature-arrow-down", "temperature-up": "temperature-arrow-up", "tenge": "tenge-sign", + "th": "table-cells", "th-large": "table-cells-large", "th-list": "table-list", - "th": "table-cells", "theater-masks": "masks-theater", "thermometer-0": "temperature-empty", "thermometer-1": "temperature-quarter", @@ -502,44 +589,50 @@ "thumb-tack": "thumbtack", "thunderstorm": "cloud-bolt", "ticket-alt": "ticket-simple", - "times-circle": "circle-xmark", - "times-rectangle": "rectangle-xmark", - "times-square": "square-xmark", "times": "xmark", - "tint-slash": "droplet-slash", + "times-circle": "circle-xmark", + "times-circle-o": "circle-xmark", + "times-rectangle": "rectangle-xmark", + "times-rectangle-o": "rectangle-xmark", + "times-square": "square-xmark", "tint": "droplet", + "tint-slash": "droplet-slash", "tired": "face-tired", "tools": "screwdriver-wrench", "torah": "scroll-torah", "tram": "cable-car", "transgender-alt": "transgender", "trash-alt": "trash-can", - "trash-restore-alt": "trash-can-arrow-up", + "trash-o": "trash-can", "trash-restore": "trash-arrow-up", + "trash-restore-alt": "trash-can-arrow-up", "triangle-circle-square": "shapes", "truck-loading": "truck-ramp-box", "try": "turkish-lira-sign", "tshirt": "shirt", "turkish-lira": "turkish-lira-sign", "tv-alt": "tv", - "undo-alt": "rotate-left", "undo": "arrow-rotate-left", + "undo-alt": "rotate-left", "university": "building-columns", "unlink": "link-slash", "unlock-alt": "unlock-keyhole", "unsorted": "sort", "usd": "dollar-sign", - "user-alt-slash": "user-large-slash", "user-alt": "user-large", + "user-alt-slash": "user-large-slash", "user-circle": "circle-user", + "user-circle-o": "circle-user", "user-cog": "user-gear", "user-edit": "user-pen", "user-friends": "user-group", "user-md": "user-doctor", + "user-o": "user", "user-times": "user-xmark", "users-cog": "users-gear", "utensil-spoon": "spoon", "vcard": "address-card", + "vcard-o": "address-card", "video-camera": "video", "volleyball-ball": "volleyball", "volume-control-phone": "phone-volume", @@ -556,6 +649,7 @@ "wifi-3": "wifi", "wifi-strong": "wifi", "window-close": "rectangle-xmark", + "window-close-o": "rectangle-xmark", "wine-glass-alt": "wine-glass-empty", "won": "won-sign", "xmark-circle": "circle-xmark", diff --git a/src/assets/fonts/moodle/moodle/agg_mean.svg b/src/assets/fonts/moodle/moodle/agg-mean.svg similarity index 99% rename from src/assets/fonts/moodle/moodle/agg_mean.svg rename to src/assets/fonts/moodle/moodle/agg-mean.svg index c10adf743..39854b56b 100644 --- a/src/assets/fonts/moodle/moodle/agg_mean.svg +++ b/src/assets/fonts/moodle/moodle/agg-mean.svg @@ -1 +1 @@ - + diff --git a/src/assets/fonts/moodle/moodle/agg_sum.svg b/src/assets/fonts/moodle/moodle/agg-sum.svg similarity index 99% rename from src/assets/fonts/moodle/moodle/agg_sum.svg rename to src/assets/fonts/moodle/moodle/agg-sum.svg index 6108dc6df..1f13bb083 100644 --- a/src/assets/fonts/moodle/moodle/agg_sum.svg +++ b/src/assets/fonts/moodle/moodle/agg-sum.svg @@ -1 +1 @@ - + diff --git a/src/core/directives/fa-icon.ts b/src/core/directives/fa-icon.ts index c80b8b643..6ac5d70d4 100644 --- a/src/core/directives/fa-icon.ts +++ b/src/core/directives/fa-icon.ts @@ -15,6 +15,7 @@ import { AfterViewInit, Directive, ElementRef, Input, OnChanges, SimpleChange } from '@angular/core'; import { CoreLogger } from '@singletons/logger'; import { CoreIcons } from '@singletons/icons'; +import { CoreConstants } from '../constants'; /** * Directive to enable font-awesome 6.4 as ionicons. @@ -41,65 +42,30 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges { } /** - * Detect icon name and use svg. + * Validate icon, e.g. checking if it's using a deprecated name. */ - async setIcon(): Promise { - let library = ''; - let iconName = this.name; - let font = 'ionicons'; - const parts = iconName.split('-', 2); - if (parts.length === 2) { - switch (parts[0]) { - case 'far': - library = 'regular'; - font = 'font-awesome'; - break; - case 'fa': - case 'fas': - library = 'solid'; - font = 'font-awesome'; - break; - case 'fab': - library = 'brands'; - font = 'font-awesome'; - break; - case 'moodle': - library = 'moodle'; - font = 'moodle'; - break; - case 'fam': - library = 'font-awesome'; - font = 'moodle'; - break; - default: - break; + async validateIcon(): Promise { + if (CoreConstants.BUILD.isDevelopment && !CoreIcons.isIconNamePrefixed(this.name)) { + this.logger.warn(`Not prefixed icon ${this.name} detected, it could be an Ionic icon. Font-awesome is preferred.`); + } + + if (this.name.includes('_')) { + // Ionic icons cannot contain a '_' in the name, Ionic doesn't load them, replace it with '-'. + this.logger.warn(`Icon ${this.name} contains '_' character and it's not allowed, replacing it with '-'.`); + this.updateName(this.name.replace(/_/g, '-')); + } + + if (this.name.match(/^fa[brs]?-/)) { + // It's a font-awesome icon, check if it's using a deprecated name. + const iconName = this.name.substring(this.name.indexOf('-') + 1); + const { fileName, newLibrary } = await CoreIcons.getFontAwesomeIconFileName(iconName); + + if (newLibrary) { + this.updateName(CoreIcons.prefixIconName('font-awesome', newLibrary, fileName)); + } else if (fileName !== iconName) { + this.updateName(this.name.replace(iconName, fileName)); } } - - if (font === 'ionicons') { - this.element.removeAttribute('src'); - this.logger.warn(`Ionic icon ${this.name} detected`); - - return; - } - - iconName = iconName.substring(parts[0].length + 1); - - // Set it here to avoid loading unexisting icon paths (svg/iconName) caused by the tick delay of the checkIconAlias promise. - let src = CoreIcons.getIconSrc(font, library, iconName); - this.element.setAttribute('src', src); - - if (font === 'font-awesome') { - const { fileName } = await CoreIcons.getFontAwesomeIconFileName(iconName); - if (fileName !== iconName) { - src = CoreIcons.getIconSrc(font, library, fileName); - this.element.setAttribute('src', src); - } - } - - this.element.classList.add('faicon'); - CoreIcons.validateIcon(this.name, src); - } /** @@ -123,7 +89,17 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges { return; } - this.setIcon(); + this.validateIcon(); + } + + /** + * Update the icon name. + * + * @param newName New name to use. + */ + protected updateName(newName: string): void { + this.name = newName; + this.element.setAttribute('name', newName); } } diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index d609d5877..d8c475ae1 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -160,8 +160,6 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreAutoLogoutService } from '@features/autologout/services/autologout'; -import '@angular/compiler'; - /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. */ @@ -206,6 +204,8 @@ export class CoreCompileProvider { viewContainerRef: ViewContainerRef, extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any ): Promise | undefined> { + // Import the Angular compiler to be able to compile components in runtime. + await import('@angular/compiler'); // Create the component using the template and the class. const component = Component({ template })(componentClass); diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 40d07e1e4..c85353011 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -540,11 +540,11 @@ export class CoreGradesHelperProvider { text = text.replace('%2F', '/').replace('%2f', '/'); if (text.indexOf('/agg_mean') > -1) { row.itemtype = 'agg_mean'; - row.icon = 'moodle-agg_mean'; + row.icon = 'moodle-agg-mean'; row.iconAlt = Translate.instant('core.grades.aggregatemean'); } else if (text.indexOf('/agg_sum') > -1) { row.itemtype = 'agg_sum'; - row.icon = 'moodle-agg_sum'; + row.icon = 'moodle-agg-sum'; row.iconAlt = Translate.instant('core.grades.aggregatesum'); } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1 || text.indexOf('fa-list-check') > -1) { row.itemtype = 'outcome'; diff --git a/src/core/initializers/initialize-icons.ts b/src/core/initializers/initialize-icons.ts new file mode 100644 index 000000000..7595267e7 --- /dev/null +++ b/src/core/initializers/initialize-icons.ts @@ -0,0 +1,22 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreIcons } from '@singletons/icons'; + +/** + * Add custom icons to Ionicons. + */ +export default function(): void { + CoreIcons.addIconsToIonicons(); +} diff --git a/src/core/singletons/icons.ts b/src/core/singletons/icons.ts index dec18251e..20860b0c4 100644 --- a/src/core/singletons/icons.ts +++ b/src/core/singletons/icons.ts @@ -12,41 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Http } from '@singletons'; import { CoreConstants } from '../constants'; import { CoreLogger } from './logger'; import aliases from '@/assets/fonts/font-awesome/aliases.json'; -import { firstValueFrom } from 'rxjs'; +import { addIcons } from 'ionicons'; +import icons from '@/assets/fonts/icons.json'; /** * Singleton with helper functions for icon management. */ export class CoreIcons { - /** - * Object used to store whether icons exist or not during development. - */ - private static readonly DEV_ICONS_STATUS: Record> = {}; - private static readonly ALIASES = { ...aliases } as unknown as Record; + private static readonly CUSTOM_ICONS = { ...icons } as unknown as Record; protected static logger = CoreLogger.getInstance('CoreIcons'); + /** + * Add custom icons to Ionicons. + */ + static addIconsToIonicons(): void { + addIcons(CoreIcons.CUSTOM_ICONS); + } + /** * Check icon alias and returns the new icon name. * * @param icon Icon name. + * @param isAppIcon Whether the icon is in the app's code, false if it's in some user generated content. * @returns New icon name and new library if changed. */ - static async getFontAwesomeIconFileName(icon: string): Promise<{fileName: string; newLibrary?: string}> { - let newLibrary: string | undefined = undefined; - if (icon.endsWith('-o')) { - newLibrary = 'regular'; - icon = icon.substring(0, icon.length - 2); - } + static async getFontAwesomeIconFileName(icon: string, isAppIcon = true): Promise<{fileName: string; newLibrary?: string}> { + const newLibrary = icon.endsWith('-o') ? 'regular' : undefined; if (CoreIcons.ALIASES[icon]) { - this.logger.error(`Icon ${icon} is an alias of ${CoreIcons.ALIASES[icon]}, please use the new name.`); + if (isAppIcon) { + this.logger.error(`Icon ${icon} is an alias of ${CoreIcons.ALIASES[icon]}, please use the new name.`); + } return { newLibrary, fileName: CoreIcons.ALIASES[icon] }; } @@ -55,30 +57,24 @@ export class CoreIcons { } /** - * Validate that an icon exists, or show warning otherwise (only in development and testing environments). + * Validate that an icon exists in the list of custom icons for the app, or show warning otherwise + * (only in development and testing environments). * - * @param name Icon name. - * @param src Icon source url. + * @param font Font Family. + * @param library Library to use. + * @param icon Icon Name. */ - static validateIcon(name: string, src: string): void { + static validateIcon(font: string, library: string, icon: string): void { if (!CoreConstants.BUILD.isDevelopment && !CoreConstants.BUILD.isTesting) { return; } - if (!(src in CoreIcons.DEV_ICONS_STATUS)) { - CoreIcons.DEV_ICONS_STATUS[src] = firstValueFrom(Http.get(src, { responseType: 'text' })) - .then(() => true) - .catch(() => false); + if ( + CoreIcons.CUSTOM_ICONS[icon] === undefined && + CoreIcons.CUSTOM_ICONS[CoreIcons.prefixIconName(font, library, icon)] === undefined + ) { + this.logger.error(`Icon ${icon} not found`); } - - // eslint-disable-next-line promise/catch-or-return - CoreIcons.DEV_ICONS_STATUS[src].then(exists => { - if (exists) { - return; - } - - return this.logger.error(`Icon ${name} not found`); - }); } /** @@ -164,7 +160,7 @@ export class CoreIcons { newIcon.setAttribute('aria-hidden', 'true'); } - const { fileName, newLibrary } = await CoreIcons.getFontAwesomeIconFileName(iconName); + const { fileName, newLibrary } = await CoreIcons.getFontAwesomeIconFileName(iconName, false); if (newLibrary) { library = newLibrary; } @@ -174,8 +170,7 @@ export class CoreIcons { newIcon.setAttribute('src', src); - newIcon.classList.add('faicon'); - CoreIcons.validateIcon(iconName, src); + CoreIcons.validateIcon('font-awesome', library, iconName); icon.parentElement?.insertBefore(newIcon, icon); icon.remove(); @@ -195,4 +190,34 @@ export class CoreIcons { return `assets/fonts/${font}/${library}/${icon}.svg`; } + /** + * Prefix an icon name using the library prefix. + * + * @param font Font Family. + * @param library Library to use. + * @param icon Icon Name. + * @returns Prefixed icon name. + */ + static prefixIconName(font: string, library: string, icon: string): string { + const prefixes = CoreConstants.CONFIG.iconsPrefixes?.[font]?.[library]; + if (!prefixes || !prefixes.length) { + return icon; + } + + return `${prefixes[0]}-${icon}`; + } + + /** + * Check if an icon name contains any of the prefixes configured for icons. + * If it doesn't then it probably is an Ionicon. + * + * @param icon Icon Name. + * @returns Whether icon is prefixed. + */ + static isIconNamePrefixed(icon: string): boolean { + return Object.values(CoreConstants.CONFIG.iconsPrefixes ?? {}) + .some(library => Object.values(library) + .some(prefixes => prefixes.some(prefix => icon.startsWith(`${prefix}-`)))); + } + } diff --git a/src/core/singletons/tests/icons.test.ts b/src/core/singletons/tests/icons.test.ts index e2abc8a90..9bd18d033 100644 --- a/src/core/singletons/tests/icons.test.ts +++ b/src/core/singletons/tests/icons.test.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreConstants } from '@/core/constants'; import { CoreIcons } from '@singletons/icons'; describe('CoreIcons singleton', () => { @@ -81,4 +82,43 @@ describe('CoreIcons singleton', () => { .toEqual('assets/fonts/font-awesome/solid/square-check.svg'); }); + it('prefixes icons names', () => { + // Arrange. + CoreConstants.CONFIG.iconsPrefixes = { + foo: { + bar: ['fo', 'for'], + baz: ['foz'], + }, + lorem: { + ipsum: ['lorip'], + }, + }; + + // Act and assert. + expect(CoreIcons.prefixIconName('foo', 'bar', 'myicon')).toEqual('fo-myicon'); + expect(CoreIcons.prefixIconName('foo', 'baz', 'myicon')).toEqual('foz-myicon'); + expect(CoreIcons.prefixIconName('lorem', 'ipsum', 'myicon')).toEqual('lorip-myicon'); + expect(CoreIcons.prefixIconName('invalid', 'invalid', 'myicon')).toEqual('myicon'); + }); + + it('check if an icon is prefixed', () => { + // Arrange. + CoreConstants.CONFIG.iconsPrefixes = { + foo: { + bar: ['fo', 'for'], + baz: ['foz'], + }, + lorem: { + ipsum: ['lorip'], + }, + }; + + // Act and assert. + expect(CoreIcons.isIconNamePrefixed('fo-myicon')).toEqual(true); + expect(CoreIcons.isIconNamePrefixed('foz-myicon')).toEqual(true); + expect(CoreIcons.isIconNamePrefixed('lorip-myicon')).toEqual(true); + expect(CoreIcons.isIconNamePrefixed('myicon')).toEqual(false); + expect(CoreIcons.isIconNamePrefixed('fox-myicon')).toEqual(false); + }); + }); diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 4c6679c9c..2b6f6ffc1 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -75,4 +75,5 @@ export interface EnvironmentConfig { disableTokenFile: boolean; // Disable the use of tokenpluginfile.php for downloading files (so it fallbacks to pluginfile.php) demoMode?: boolean; // Whether to run the app in "demo mode". hideInformativeLinks?: boolean; // Whether to hide informative links. + iconsPrefixes?: Record>; // Prefixes for custom font icons (located in src/assets/fonts). }