@migliori/universal-icon-picker
Version:
Vanilla JS Icon Picker for any Icon Library
513 lines (450 loc) • 23.4 kB
JavaScript
/* eslint-disable no-console */
/*jshint esversion: 8 */
const scriptUrl = new URL(document.currentScript.src);
const iconPickerUrl = scriptUrl.origin + scriptUrl.pathname.substring(0, scriptUrl.pathname.lastIndexOf('/js') + 1);
const loadedDependencies = [];
const i18nMessages = {
en: {
all_icons: "All icons",
all_label: "All",
close_label: "Close",
icon_picker: "Universal Icon Picker",
insert_label: "Insert",
search_label: "Search",
search_placeholder: "Filter by name…"
},
fr: {
all_icons: "Toutes les icônes",
all_label: "Tout",
close_label: "Fermer",
icon_picker: "Sélecteur d'icônes universel",
insert_label: "Insérer",
search_label: "Rechercher",
search_placeholder: "Filtrer par nom…"
}
};
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory('UniversalIconPicker'));
} else if (typeof exports === 'object') {
module.exports = factory('UniversalIconPicker');
} else {
root['UniversalIconPicker'] = factory('UniversalIconPicker');
}
}(this, function () {
'use strict';
const createDomEle = function (string) {
const ele = document.createElement('div');
ele.innerHTML = string;
return ele.firstChild;
};
const debounce = function (func, wait, immediate) {
let timeout;
return function () {
const context = this,
args = arguments;
const later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
const escapeHtml = function (text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'\'': ''',
};
return text.replace(/[&<>"']/g, function (m) {
return map[m];
});
};
/**
* Merge defaults with user options
* @param {Object} defaults Default settings
* @param {Object} options User options
*/
const extend = function (defaults, options) {
let prop, extended = {};
for (prop in defaults) {
if (Object.prototype.hasOwnProperty.call(defaults, prop)) {
extended[prop] = defaults[prop];
}
}
for (prop in options) {
if (Object.prototype.hasOwnProperty.call(options, prop)) {
extended[prop] = options[prop];
}
}
return extended;
};
const getLibraryName = function (string) {
return string.replace(/([A-Z])/g, ' $1');
};
/**
* Plugin Object
* @param selector The html selector to initialize
* @param {Object} options User options
* @constructor
*/
function UniversalIconPicker(selector, options) {
this.selector = selector;
let defaults = {
allowEmpty: true,
iconLibraries: null,
iconLibrariesCss: null,
mode: 'autoload', // autoload | onrequest,
parentElement: 'body',
onBeforeOpen: null,
onReset: null,
onSelect: null,
resetSelector: null,
language: navigator.language || navigator.userLanguage,
loadCustomCss: false
};
this.options = extend(defaults, options);
this.activeLibraryId = '';
this.filterIcon = '';
this.iconEventsLoaded = false;
this.iconLibraries = {};
this.iconLibrariesLoaded = false;
this.iconMarkup = '';
this.iconWrap = '';
this.idSuffix = '-' + this.selector.replace(/[#\s[\]="]/g, '');
this.sideBarBtn = '';
this.sideBarList = [];
// Set language (force lowercase and remove country code if present, defaults to English if code is not found)
let language = this.options.language.toLowerCase().split('-')[0].split('_')[0];
if (language in i18nMessages) {
this.messages = i18nMessages[language];
} else {
this.messages = i18nMessages["en"];
}
this.universalWrap = '<div class="uip-modal uip-open" id="uip-modal' + this.idSuffix + '"><div class="uip-modal--content"><div class="uip-modal--header"><div class="uip-modal--header-logo-area"><span class="uip-modal--header-logo-title">' + this.messages.icon_picker + '</span></div><div class="uip-modal--header-close-btn"><img src="' + (options.closeUrl || iconPickerUrl + '/images/xmark-solid.svg') + '" width="20" height="16" alt="' + this.messages.close_label + '" title="' + this.messages.close_label + '" /></div></div><div class="uip-modal--body"><div id="uip-modal--sidebar' + this.idSuffix + '" class="uip-modal--sidebar"><div class="uip-modal--sidebar-tabs"></div></div><div id="uip-modal--icon-preview-wrap' + this.idSuffix + '" class="uip-modal--icon-preview-wrap"><div class="uip-modal--icon-search"><input name="" value="" placeholder="' + this.messages.search_placeholder + '"><img src="' + (options.searchUrl || iconPickerUrl + '/images/magnifying-glass-solid.svg') + '" width="20" height="16" alt="' + this.messages.search_label + '" title="' + this.messages.search_label + '" /></div><div class="uip-modal--icon-preview-inner"><div id="uip-modal--icon-preview' + this.idSuffix + '" class="uip-modal--icon-preview"></div></div></div></div><div class="uip-modal--footer"><button type="button" class="uip-insert-icon-button">' + this.messages.insert_label + '</button></div></div></div>';
this.universalDomEle = createDomEle(this.universalWrap);
this.sidebarTabs = this.universalDomEle.querySelector('.uip-modal--sidebar-tabs');
this.previewWrap = this.universalDomEle.querySelector('#uip-modal--icon-preview' + this.idSuffix);
this.searchInput = this.universalDomEle.querySelector('.uip-modal--icon-search input');
if (this.options.mode === 'autoload') {
this.init();
} else {
document.querySelector(this.selector).addEventListener('click', this.init.bind(this), { once: true });
}
}
// Plugin prototype
UniversalIconPicker.prototype = {
/* Public functions
-------------------------------------------------- */
init: function () {
this._loadCssFiles();
if (this.options.mode !== 'autoload') {
this._onBeforeOpen().then(() => {
this.open();
});
}
document.querySelector(this.selector).addEventListener('click', () => {
this._onBeforeOpen().then(() => {
this.open();
});
});
//Remove selected icon
if (this.options.resetSelector) {
document.querySelector(this.options.resetSelector).addEventListener('click', this.options.onReset);
}
},
open: function () {
this._loadIconLibraries().then(() => {
this.iconLibrariesLoaded = true;
if (!document.getElementById('uip-modal' + this.idSuffix)) {
//push universal dom to body
document.querySelector(this.options.parentElement).appendChild(this.universalDomEle);
//Icon library close by clicking close button
this.universalDomEle.querySelector('.uip-modal--header-close-btn').addEventListener('click', () => {
this.universalDomEle.classList.add('uip-close');
this.universalDomEle.classList.remove('uip-open');
});
//Insert button
this.universalDomEle.querySelector('.uip-insert-icon-button').addEventListener('click', () => {
let selected = this.universalDomEle.querySelector('.universal-selected');
if (selected) {
let iconHtml = selected.querySelector('i').outerHTML;
let jsonOutput = {
'libraryId': selected.dataset.libraryId,
'libraryName': selected.dataset.libraryName,
'iconHtml': null,
'iconMarkup': null,
'iconClass': null,
'iconText': null
};
if (!selected.querySelector('i').classList.value.match('uip-icon-none')) {
jsonOutput.iconHtml = iconHtml;
jsonOutput.iconMarkup = escapeHtml(iconHtml);
jsonOutput.iconClass = selected.querySelector('i').classList.value;
jsonOutput.iconText = selected.querySelector('i').innerText;
}
this.options.onSelect(jsonOutput);
}
this.universalDomEle.classList.add('uip-close');
this.universalDomEle.classList.remove('uip-open');
});
} else {
//Icon library open if dom element exist
this.universalDomEle.classList.remove('uip-close');
this.universalDomEle.classList.add('uip-open');
}
if (!this.iconEventsLoaded) {
// selected icon highlighted by adding class
this.universalDomEle.querySelectorAll('.uip-icon-item').forEach((item) => {
item.addEventListener('click', (evt) => {
this.iconWrap.forEach((el) => {
el.classList.remove('universal-selected');
});
evt.currentTarget.classList.toggle('universal-selected');
});
item.addEventListener('dblclick', (evt) => {
this.universalDomEle.querySelector('.uip-insert-icon-button').click();
});
});
this.iconEventsLoaded = true;
}
this.universalDomEle.querySelector('.uip-modal--icon-search input').focus();
});
},
setOptions: function (opts) {
this.options = extend(this.options, opts);
if (opts.iconLibrariesCss) {
this._loadCssFiles();
}
if (opts.iconLibraries) {
// dom icon events need to be reloaded
this.iconEventsLoaded = false;
this.iconLibrariesLoaded = false;
this._resetIconAndSidebarList();
}
},
/* Private functions
-------------------------------------------------- */
_clickHandlerFunc: function (e) {
if (!e.currentTarget.classList.contains('universal-active')) {
this.sideBarBtn.forEach(function (item) {
item.classList.remove('universal-active');
});
e.currentTarget.classList.add('universal-active');
}
this._sidebarFilterFunc(e.currentTarget.dataset['libraryId']);
},
_iconItemMarkup: function (libraryName, libraryItem) {
let markup = '',
library = libraryItem['icon-style'],
prefix = libraryItem['prefix'];
if (this.options.allowEmpty) {
markup += '<div class="uip-icon-item" data-library-id="' + library + '" data-filter="" data-library-name="' + libraryName + '"><div class="uip-icon-item-inner"><i class="' + prefix + ' uip-icon-none"> </i><div class="uip-icon-item-name" title="None">None</div></div></div>';
}
if (prefix.match(/^material-icons/)) {
libraryItem['icons'].forEach(function (item) {
markup += '<div class="uip-icon-item" data-library-id="' + library + '" data-filter="' + item + '" data-library-name="' + libraryName + '"><div class="uip-icon-item-inner"><i class="' + prefix + '">' + item + '</i><div class="uip-icon-item-name" title="' + item + '">' + item.replace('-', ' ') + '</div></div></div>';
});
} else {
libraryItem['icons'].forEach(function (item) {
markup += '<div class="uip-icon-item" data-library-id="' + library + '" data-filter="' + item + '" data-library-name="' + libraryName + '"><div class="uip-icon-item-inner"><i class="' + [prefix, item].join('') + '"></i><div class="uip-icon-item-name" title="' + item + '">' + item.replace('-', ' ') + '</div></div></div>';
});
}
return markup;
},
_iconItemPush: function (arrayList) {
this.previewWrap.innerHTML = '';
arrayList.forEach((item) => {
this.previewWrap.appendChild(item[1]);
});
},
_loadCssFiles: function () {
let link = document.createElement('link');
if (!loadedDependencies.includes('universal-icon-picker.min.css') && !this.options.loadCustomCss) {
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = iconPickerUrl + 'stylesheets/universal-icon-picker.min.css';
link.media = 'screen';
document.head.appendChild(link);
loadedDependencies.push('universal-icon-picker.min.css');
}
if (this.options.iconLibrariesCss) {
this.options.iconLibrariesCss.forEach(cssFile => {
if (!loadedDependencies.includes(cssFile)) {
let cssFileLink = iconPickerUrl + 'stylesheets/' + cssFile;
if (cssFile.match(/^http|^\/\//)) {
cssFileLink = cssFile;
}
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = cssFileLink;
link.media = 'screen';
document.head.appendChild(link);
loadedDependencies.push(cssFile);
}
});
}
},
_loadIconLibraries: async function (i = 0) {
if (!this.options.iconLibraries) {
console.error('Universal icon picker - no icon library loaded');
return false;
}
if (this.iconLibrariesLoaded) {
return true;
}
if (i === 0 && this.options.iconLibraries.length > 1) {
this.sideBarList.push({
"title": this.messages.all_icons,
'list-icon': '',
'library-id': 'all',
'prefix': '',
});
}
let iconLibUrl = this.options.iconLibraries[i];
let iconLib;
if (iconLibUrl.includes('/')) {
iconLib = /[^/]*$/.exec(iconLibUrl)[0];
iconLib = /^[^.]*/.exec(iconLib)[0];
} else {
iconLibUrl = iconPickerUrl + 'icons-libraries/' + iconLibUrl;
iconLib = iconLibUrl.substring(iconLibUrl.lastIndexOf('/') + 1);
}
await fetch(iconLibUrl)
.then(response => response.json())
.then(data => {
// Success!
const camelCasedIconLibrary = iconLib.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }).replace(/\.[a-z.]+$/, '');
let newLibrary = {};
newLibrary[camelCasedIconLibrary] = data;
Object.assign(this.iconLibraries, newLibrary); // new icon library merge
if (i + 1 === this.options.iconLibraries.length) {
//set icon and sidebar list
this._setIconAndSidebarList();
this.activeLibraryId = this.sideBarList[0]['library-id'];
//sidebar list markup push
this.sidebarTabs.innerHTML = this._sideBarListMarkup(this.sideBarList);
//icon markup push
this.previewWrap.innerHTML = this.iconMarkup;
// get all icon wrapper dom element
this.iconWrap = this.previewWrap.querySelectorAll('.uip-icon-item');
//set event lisner to search input
this.searchInput.addEventListener('keyup', debounce(this._searchFunc, 100).bind(this), false);
//get all sidebar list item wrapper dom element
this.sideBarBtn = this.sidebarTabs.querySelectorAll('.uip-modal--sidebar-tab-item');
//set click event lisner to sidebar list item
this.sideBarBtn.forEach((item) => {
item.addEventListener('click', this._clickHandlerFunc.bind(this), false);
});
return true;
} else {
return this._loadIconLibraries(i + 1);
}
}).catch((error) => {
console.log(error);
return error;
});
},
_onBeforeOpen: async function () {
if (typeof (this.options.onBeforeOpen) === 'function') {
return this.options.onBeforeOpen();
}
},
_resetIconAndSidebarList: function () {
this.sideBarList = [];
this.iconMarkup = '';
this.iconLibraries = {};
this.iconWrap = '';
this.filterIcon = '';
this.sideBarBtn = '';
this.activeLibraryId = '';
},
_searchFunc: function (e) {
// console.log(this.value.toLowerCase());
const searchText = e.target.value.toLowerCase();
this._searchFilterFunc(searchText, 'filter');
},
_searchFilterFunc: function (filterText, dataName) {
this.filterIcon = Object.entries(this.iconWrap).filter((item) => {
if (-1 === item[1].dataset[dataName].indexOf(filterText) || (this.activeLibraryId !== 'all' && item[1].dataset['libraryId'] !== this.activeLibraryId)) {
return false;
}
return true;
});
this._iconItemPush(this.filterIcon);
},
_setIconAndSidebarList: function () {
for (const [libraryName, libraryContent] of Object.entries(this.iconLibraries)) {
this._setSideBarList(libraryContent.verboseName || getLibraryName(libraryName), libraryContent);
this._setIconMarkup(libraryName, libraryContent);
}
},
_setIconMarkup: function (libraryName, libraryContent) {
if (libraryContent.icons !== undefined) {
this.iconMarkup += this._iconItemMarkup(libraryName, libraryContent);
} else {
Object.entries(libraryContent).forEach((item) => {
this.iconMarkup += this._iconItemMarkup(libraryName, item[1]);
});
}
},
_sidebarFilterFunc: function (filterText) {
this.activeLibraryId = filterText;
this.filterIcon = Object.entries(this.iconWrap).filter(function (item) {
if ('all' === filterText || filterText === item[1].dataset['libraryId']) {
return true;
}
return false;
});
this._iconItemPush(this.filterIcon);
},
_setSideBarList: function (libraryName, libraryContent) {
let listItem;
if (libraryContent.icons !== undefined) {
listItem = {
'title': libraryName,
'prefix': libraryContent['prefix'] !== undefined ? libraryContent['prefix'] : '',
'list-icon': libraryContent['list-icon'] !== undefined ? libraryContent['list-icon'] : '',
'library-id': libraryContent['icon-style'] !== undefined ? libraryContent['icon-style'] : 'all',
};
this.sideBarList.push(listItem);
} else {
Object.entries(libraryContent).forEach(item => {
listItem = {
'title': libraryName + ' - ' + item[0],
'prefix': item[1]['prefix'] !== undefined ? item[1]['prefix'] : '',
'list-icon': item[1]['list-icon'] !== undefined ? item[1]['list-icon'] : '',
'library-id': item[1]['icon-style'] !== undefined ? item[1]['icon-style'] : 'all',
};
this.sideBarList.push(listItem);
});
}
},
_sideBarListMarkup: function (sideBarList) {
let markup = '';
sideBarList.forEach((item) => {
let activeClazz = '';
if (item['library-id'] === this.activeLibraryId) {
activeClazz = ' universal-active';
}
if ('all' !== item['library-id']) {
let iconTag = '<i class="' + item['list-icon'] + '"></i>';
if (item['prefix'].match(/^material-icons/)) {
iconTag = '<i class="' + item['prefix'] + '">' + item['list-icon'] + '</i>';
}
markup += '<div class="uip-modal--sidebar-tab-item' + activeClazz + '" data-library-id="' + item['library-id'] + '">' + iconTag + item['title'] + '</div>';
} else {
markup += '<div class="uip-modal--sidebar-tab-item' + activeClazz + '" data-library-id="' + item['library-id'] + '"><img src="' + (this.options.starUrl || iconPickerUrl + '/images/star-of-life-solid.svg') + '" width="13.125px" height="auto" alt="' + this.messages.all_label + '" title="' + this.messages.all_label + '" />' + item['title'] + '</div>';
}
});
return markup;
},
};
return UniversalIconPicker;
}));