UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

779 lines (686 loc) 27 kB
import { dom, keyCodeMap } from '../../helper'; import { _w } from '../../helper/env'; import ApiManager from '../manager/ApiManager'; /** * Browser file item structure * @typedef {Object} BrowserFile * @property {string} [src=""] - Source url * @property {string} [name=""] - File name | Folder name * @property {string} [thumbnail] - Thumbnail url * @property {string} [alt] - Image alt * @property {Array<string>|string} [tag] - Tag name list * @property {string} [type] - Type (image, video, audio, etc.) * @property {string} [frame] - Frame name (iframe, video, etc.) * @property {boolean} [default] - Whether this folder is the default selection. * @property {Object<string, *>} [meta] - Metadata * @property {BrowserFile | string} [_data] - Internal: The folder's contents or an API URL (⚠️ DO NOT USE directly) */ /** * @typedef BrowserParams * @property {string} title - File browser window title. Required. Can be overridden in browser. * @property {string} [className] - Class name of the file browser. Optional. Default: ''. * @property {Object<string, *>|Array<*>} [data] - direct data without server calls * @property {string} [url] - File server url. Required. Can be overridden in browser. * @property {Object<string, string>} [headers] - File server http header. Required. Can be overridden in browser. * @property {(target: Node) => void} selectorHandler - Function that actions when an item is clicked. Required. Can be overridden in browser. * @property {boolean} [useSearch] - Whether to use the search function. Optional. Default: `true`. * @property {string} [searchUrl] - File server search url. Optional. Can be overridden in browser. * - Requested as `searchUrl + '?keyword=' + keyword`. The server must return: * ```js * { * "result": [ * { * "src": "https://example.com/file.jpg", * "name": "file.jpg", * "thumbnail": "https://example.com/file_thumb.jpg", * "tag": ["photo"] * } * ] * } * ``` * @property {Object<string, string>} [searchUrlHeader] - File server search http header. Optional. Can be overridden in browser. * @property {string} [listClass] - Class name of list div. Required. Can be overridden in browser. * @property {(item: BrowserFile) => string} [drawItemHandler] - Function that returns HTML string for rendering each file item. Required. Can be overridden in browser. * ```js * // drawItemHandler * (item) => `<div><img src="${item.thumbnail}"><span>${item.name}</span></div>` * ``` * @property {Array<*>} [props] - `props` argument to `drawItemHandler` function. Optional. Can be overridden in browser. * @property {number} [columnSize] - Number of `div.se-file-item-column` to be created. * - Optional. Can be overridden in browser. Default: 4. * @property {number} [expand=1] - Initial folder expand depth. `1` expands the first level, `Infinity` expands all. Default: `1`. * @property {((item: BrowserFile) => string)} [thumbnail] - Default thumbnail */ /** * @class * @description File browser plugin */ class Browser { #$; #loading; #globalEventHandler; /** @type {Array<BrowserFile>} */ #allItems = []; #searchInput; #searchClearBtn; #closeSignal = false; #bindClose = null; /** * @constructor * @param {*} host The instance object that called the constructor. * @param {SunEditor.Deps} $ Kernel dependencies * @param {BrowserParams} params Browser options * @example * // Inside a PluginBrowser constructor: * this.browser = new Browser(this, this.$, { * title: this.$.lang.imageGallery, * data: pluginOptions.data, * url: pluginOptions.url, * headers: pluginOptions.headers, * selectorHandler: this.#OnSelect.bind(this), * columnSize: 4, * className: 'se-image-gallery', * }); */ constructor(host, $, params) { this.#$ = $; // create HTML this.useSearch = params.useSearch ?? true; const browserFrame = dom.utils.createElement('DIV', { class: 'se-browser sun-editor-common' + (params.className ? ` ${params.className}` : '') }); const contentHTML = CreateHTMLInfos(this.#$, this.useSearch); const content = contentHTML.html; // members this.kind = host.constructor['key'] || host.constructor.name; this.host = host; this.area = browserFrame; this.header = contentHTML.header; this.titleArea = contentHTML.titleArea; this.tagArea = contentHTML.tagArea; this.body = contentHTML.body; this.list = contentHTML.list; this.side = contentHTML.side; this.wrapper = contentHTML.wrapper; this.#loading = contentHTML._loading; this.title = params.title; this.listClass = params.listClass || 'se-preview-list'; this.directData = params.data; this.url = params.url; this.urlHeader = params.headers; this.searchUrl = params.searchUrl; this.searchUrlHeader = params.searchUrlHeader; this.drawItemHandler = (params.drawItemHandler || DrawItems).bind({ thumbnail: params.thumbnail, props: params.props || [] }); this.selectorHandler = params.selectorHandler; this.columnSize = params.columnSize || 4; this.expand = params.expand ?? 1; this.folderDefaultPath = ''; this.closeArrow = this.#$.icons.menu_arrow_right; this.openArrow = this.#$.icons.menu_arrow_down; this.icon_folder = this.#$.icons.side_menu_folder_item; this.icon_folder_item = this.#$.icons.side_menu_folder; this.icon_item = this.#$.icons.side_menu_item; /** @type {Array<BrowserFile>} */ this.items = []; /** @type {Object<string, {name: string, meta: Object<string, *>}>} */ this.folders = {}; /** @type {Object<string, {key?: string, name?: string, children?: *}>} */ this.tree = {}; /** @type {BrowserFile} */ this.data = {}; this.selectedTags = []; this.keyword = ''; this.sideInner = null; // api manager this.apiManager = new ApiManager(this, $, { method: 'GET' }); this.#globalEventHandler = (e) => { if (!keyCodeMap.isEsc(e.code)) return; this.close(); }; // init browserFrame.appendChild(dom.utils.createElement('DIV', { class: 'se-browser-back' })); browserFrame.appendChild(content); this.#$.contextProvider.carrierWrapper.appendChild(browserFrame); this.#$.eventManager.addEvent(this.tagArea, 'click', this.#OnClickTag.bind(this)); this.#$.eventManager.addEvent(this.list, 'click', this.#OnClickFile.bind(this)); this.#$.eventManager.addEvent(this.side, 'click', this.#OnClickSide.bind(this)); this.#$.eventManager.addEvent(content, 'mousedown', this.#OnMouseDown_browser.bind(this)); this.#$.eventManager.addEvent(content, 'click', this.#OnClick_browser.bind(this)); this.#$.eventManager.addEvent((this.sideOpenBtn = /** @type {HTMLButtonElement} */ (browserFrame.querySelector('.se-side-open-btn'))), 'click', this.#SideOpen.bind(this)); this.#$.eventManager.addEvent([this.header, browserFrame.querySelector('.se-browser-main')], 'mousedown', this.#SideClose.bind(this)); // search const searchForm = browserFrame.querySelector('form.se-browser-search-form'); this.#searchInput = /** @type {HTMLInputElement} */ (searchForm?.querySelector('input[type="text"]')); this.#searchClearBtn = /** @type {HTMLButtonElement} */ (browserFrame.querySelector('.se-browser-search-clear')); this.#$.eventManager.addEvent(searchForm, 'submit', this.#Search.bind(this)); this.#$.eventManager.addEvent(this.#searchClearBtn, 'click', this.#ClearSearch.bind(this)); } /** * @description Open a file browser plugin * @param {Object} [params={}] * @param {string} [params.listClass] - Class name of list div. If not, use `this.listClass`. * @param {string} [params.title] - File browser window title. If not, use `this.title`. * @param {string} [params.url] - File server url. If not, use `this.url`. * @param {Object<string, string>} [params.urlHeader] - File server http header. If not, use `this.urlHeader`. * @example * // Open with default settings (configured at construction): * this.browser.open(); * * // Open with runtime overrides: * this.browser.open({ * title: 'Select a video', * url: '/api/videos', * urlHeader: { Authorization: 'Bearer token' }, * }); */ open(params = {}) { this.#addGlobalEvent(); const listClassName = params.listClass || this.listClass; if (!dom.utils.hasClass(this.list, listClassName)) { this.list.className = 'se-browser-list ' + listClassName; } this.titleArea.textContent = params.title || this.title; this.area.style.display = 'block'; this.#$.ui.opendBrowser = this; this.closeArrow = this.#$.options.get('_rtl') ? this.#$.icons.menu_arrow_left : this.#$.icons.menu_arrow_right; if (this.directData) { this.#drowItems(this.directData); } else { this.#drawFileList(params.url || this.url, params.urlHeader || this.urlHeader, false); } this.body.style.maxHeight = dom.utils.getClientSize().h - (this.#$.offset.getGlobal(this.body).top - _w.scrollY) - 20 + 'px'; } /** * @description Close a browser plugin * - The plugin's `init` method is called. */ close() { this.#removeGlobalEvent(); this.apiManager.cancel(); this.area.style.display = 'none'; this.selectedTags = []; this.items = []; this.#allItems = []; this.folders = {}; this.tree = {}; this.data = {}; this.keyword = ''; this.list.innerHTML = this.tagArea.innerHTML = this.titleArea.textContent = ''; if (this.#searchInput) this.#searchInput.value = ''; if (this.#searchClearBtn) this.#searchClearBtn.style.display = 'none'; this.#$.ui.opendBrowser = null; this.sideInner = null; this.host.browserInit?.(); } /** * @description Search files * @param {string} keyword - Search keyword */ search(keyword) { if (this.searchUrl) { this.keyword = keyword; this.#drawFileList(this.searchUrl + '?keyword=' + keyword, this.searchUrlHeader, false); } else { this.keyword = keyword.toLowerCase(); this.#drawListItem(this.#allItems.length > 0 ? this.#allItems : this.items, false); } } /** * @description Collects all file items from every folder in `this.data`. * @returns {Array<BrowserFile>} */ #collectAllItems() { const all = []; for (const key in this.data) { const items = this.data[key]; if (Array.isArray(items)) { for (let i = 0; i < items.length; i++) { all.push(items[i]); } } } return all; } /** * @description Filter items by tag * @param {Array<BrowserFile>} items - Items to filter * @returns {Array<BrowserFile>} * @example * // Filter items by currently selected tags: * browser.selectedTags = ['photo', 'landscape']; * const filtered = browser.tagfilter(items); * // Returns only items whose `tag` array includes 'photo' or 'landscape' */ tagfilter(items) { const selectedTags = this.selectedTags; return selectedTags.length === 0 ? items : items.filter((item) => !Array.isArray(item.tag) || item.tag.some((tag) => selectedTags.includes(tag))); } /** * @description Show file browser loading box */ showBrowserLoading() { this.#loading.style.display = 'block'; } /** * @description Close file browser loading box */ closeBrowserLoading() { this.#loading.style.display = 'none'; } /** * @description Fetches the file list from the server. * @param {string} url - The file server URL. * @param {Object<string, string>} urlHeader - The HTTP headers for the request. * @param {boolean} pageLoading - Indicates if this is a paginated request. */ #drawFileList(url, urlHeader, pageLoading) { this.apiManager.call({ method: 'GET', url, headers: urlHeader, callBack: this.#CallBackGet.bind(this), errorCallBack: this.#CallBackError.bind(this) }); if (!pageLoading) { this.sideOpenBtn.style.display = 'none'; this.showBrowserLoading(); } } /** * @description Updates the displayed list of file items. * @param {Array<BrowserFile>} items - The file items to display. * @param {boolean} update - Whether to update the tags. */ #drawListItem(items, update) { const keyword = this.keyword; items = this.tagfilter(items).filter((item) => item.name.toLowerCase().indexOf(keyword) > -1); const _tags = []; const len = items.length; const columnSize = this.columnSize; const splitSize = columnSize <= 1 ? 1 : Math.round(len / columnSize) || 1; const drawItemHandler = this.drawItemHandler; let tagsHTML = ''; let listHTML = '<div class="se-file-item-column">'; let columns = 1; for (let i = 0, item, tags; i < len; i++) { item = items[i]; tags = !item.tag ? [] : typeof item.tag === 'string' ? item.tag.split(',') : item.tag; tags = item.tag = tags.map((v) => v.trim()); listHTML += drawItemHandler(item); if ((i + 1) % splitSize === 0 && columns < columnSize && i + 1 < len) { columns++; listHTML += '</div><div class="se-file-item-column">'; } if (update && tags.length > 0) { for (let t = 0, tLen = tags.length, tag; t < tLen; t++) { tag = tags[t]; if (tag && !_tags.includes(tag)) { _tags.push(tag); tagsHTML += `<a title="${tag}" aria-label="${tag}">${tag}</a>`; } } } } listHTML += '</div>'; this.list.innerHTML = listHTML; if (keyword) { this.#highlightKeyword(keyword); } if (update) { this.items = items; this.tagArea.innerHTML = tagsHTML; } } /** * @description Adds a global event listener for closing the browser. */ #addGlobalEvent() { this.#removeGlobalEvent(); this.#bindClose = this.#$.eventManager.addGlobalEvent('keydown', this.#globalEventHandler, true); } /** * @description Removes the global event listener for closing the browser. */ #removeGlobalEvent() { this.#bindClose &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose); } /** * @description Renders the file items or folder structure from data. * @param {BrowserFile[]|BrowserFile} data - The data representing the file structure. * @returns {boolean} `true` if rendering was successful, `false` otherwise. */ #drowItems(data) { if (Array.isArray(data)) { if (data.length > 0) { this.#drawListItem(data, true); } return true; } else if (typeof data === 'object') { this.sideOpenBtn.style.display = ''; this.#parseFolderData(data); this.#allItems = this.#collectAllItems(); this.side.innerHTML = ''; const sideInner = (this.sideInner = dom.utils.createElement('div', null)); this.#createFolderList(this.tree, sideInner); this.side.appendChild(sideInner); if (this.folderDefaultPath) { const openFolder = /** @type {HTMLButtonElement} */ (sideInner.querySelector(`[data-command="${this.folderDefaultPath}"]`)); openFolder.click(); if (this.folderDefaultPath.includes('/')) { dom.utils.removeClass(openFolder.parentElement, 'se-menu-hidden'); openFolder.parentElement.previousElementSibling.querySelector('button').innerHTML = this.openArrow; } } return true; } return false; } /** * @description Parses folder data into a structured format. * @param {BrowserFile} data - The folder data. * @param {string} [path] - The current path in the folder hierarchy. */ #parseFolderData(data, path) { let current = this.tree; // _data if (data._data) { this.data[path] = data._data; if (!this.folderDefaultPath || data.default) { this.folderDefaultPath = path; } const parts = path.split('/'); const len = parts.length - 1; parts.forEach((part, index) => { current[part] ||= { children: {} }; if (index === len) { current[part].key = path; current[part].name = this.folders[path].name; } else { current = current[part].children; } }); } else if (path) { current[path] = { name: this.folders[path].name, children: {} }; } // create folders, file path Object.entries(data).forEach(([key, value]) => { if (key === '_data' || !value || typeof value !== 'object') return; const v = /** @type {BrowserFile} */ (value); const currentPath = path ? `${path}/${key}` : key; this.folders[currentPath] = { name: v.name || key, meta: v.meta || {}, }; this.#parseFolderData(v, currentPath); }); } /** * @description Creates a nested folder list from parsed data. * @param {BrowserFile[]|BrowserFile} folderData - The structured folder data. * @param {HTMLElement} parentElement - The parent element to append folder structure to. * @param {number} [depth=0] - Current depth level. */ #createFolderList(folderData, parentElement, depth = 0) { const expanded = depth < this.expand; for (const key in folderData) { const item = folderData[key]; if (!item) continue; if (Object.keys(item.children).length > 0) { const folderLabel = dom.utils.createElement( 'div', item.key ? { 'data-command': item.key, 'aria-label': item.name } : null, `<span class="se-menu-icon">${item.key ? this.icon_folder : this.icon_folder_item}</span><span>${item.name}</span>`, ); const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel); folderLabel.insertBefore(dom.utils.createElement('button', null, expanded ? this.openArrow : this.closeArrow), folderLabel.firstElementChild); const childContainer = document.createElement('div'); dom.utils.addClass(childContainer, expanded ? 'se-menu-child' : 'se-menu-child|se-menu-hidden'); this.#createFolderList(item.children, childContainer, depth + 1); folderDiv.appendChild(childContainer); parentElement.appendChild(folderDiv); } else { const folderLabel = dom.utils.createElement('div', { 'data-command': item.key, 'aria-label': item.name, class: 'se-menu-folder-item' }, `<span class="se-menu-icon">${this.icon_item}</span><span>${item.name}</span>`); if (parentElement === this.sideInner) { const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel); parentElement.appendChild(folderDiv); } else { parentElement.appendChild(folderLabel); } } } } /** * @param {XMLHttpRequest} xmlHttp - XMLHttpRequest object. */ #CallBackGet(xmlHttp) { try { const res = JSON.parse(xmlHttp.responseText); const data = res.result; if (this.#drowItems(data)) return; if (res.nullMessage) { this.list.innerHTML = res.nullMessage; } } catch (e) { throw Error(`[SUNEDITOR.browser.drawList.fail] cause: "${e.message}"`); } finally { this.closeBrowserLoading(); } } /** * @param {*} res - response data. * @param {XMLHttpRequest} xmlHttp - XMLHttpRequest object. */ #CallBackError(res, xmlHttp) { this.closeBrowserLoading(); throw Error(`[SUNEDITOR.browser.get.serverException] status: ${xmlHttp.status}, response: ${res.errorMessage || xmlHttp.responseText}`); } /** * @param {MouseEvent} e - Event object */ #OnClickTag(e) { const eventTarget = dom.query.getEventTarget(e); if (!dom.check.isAnchor(eventTarget)) return; const tagName = eventTarget.textContent; const selectTag = this.tagArea.querySelector('a[title="' + tagName + '"]'); const sTagIndex = this.selectedTags.indexOf(tagName); if (sTagIndex > -1) { this.selectedTags.splice(sTagIndex, 1); dom.utils.removeClass(selectTag, 'on'); } else { this.selectedTags.push(tagName); dom.utils.addClass(selectTag, 'on'); } this.#drawListItem(this.items, false); } /** * @param {MouseEvent} e - Event object */ #OnClickFile(e) { const eventTarget = dom.query.getEventTarget(e); e.preventDefault(); e.stopPropagation(); if (eventTarget === this.list) return; const target = dom.query.getCommandTarget(eventTarget); if (!target) return; this.close(); this.selectorHandler(target); } /** * @param {MouseEvent} e - Event object */ #OnClickSide(e) { const eventTarget = dom.query.getEventTarget(e); e.stopPropagation(); if (/^button$/i.test(eventTarget.nodeName)) { const childContainer = eventTarget.parentElement.parentElement.querySelector('.se-menu-child'); if (dom.utils.hasClass(childContainer, 'se-menu-hidden')) { dom.utils.removeClass(childContainer, 'se-menu-hidden'); eventTarget.innerHTML = this.openArrow; } else { dom.utils.addClass(childContainer, 'se-menu-hidden'); eventTarget.innerHTML = this.closeArrow; } return; } const cmdTarget = dom.query.getCommandTarget(eventTarget); if (!cmdTarget || dom.utils.hasClass(cmdTarget, 'active')) return; const data = this.data[cmdTarget.getAttribute('data-command')]; dom.utils.removeClass(this.side.querySelectorAll('.active'), 'active'); dom.utils.addClass([cmdTarget, dom.query.getParentElement(cmdTarget, '.se-menu-folder')], 'active'); this.tagArea.innerHTML = ''; if (typeof data === 'string') { this.#drawFileList(data, this.urlHeader, true); } else { this.#drawListItem(data, true); } } /** * @param {MouseEvent} e - Event object */ #OnMouseDown_browser(e) { const eventTarget = dom.query.getEventTarget(e); if (/se-browser-inner/.test(eventTarget.className)) { this.#closeSignal = true; } else { this.#closeSignal = false; } } /** * @param {MouseEvent} e - Event object */ #OnClick_browser(e) { const eventTarget = dom.query.getEventTarget(e); e.stopPropagation(); if (/close/.test(eventTarget.getAttribute('data-command')) || this.#closeSignal) { this.close(); } } /** * @param {SubmitEvent} e - Event object */ #Search(e) { e.preventDefault(); const keyword = this.#searchInput.value; this.search(keyword); if (this.#searchClearBtn) this.#searchClearBtn.style.display = keyword ? '' : 'none'; } /** * @description Clears the search keyword and restores the current folder's item list. */ #ClearSearch() { if (this.#searchInput) this.#searchInput.value = ''; if (this.#searchClearBtn) this.#searchClearBtn.style.display = 'none'; this.keyword = ''; this.#drawListItem(this.items, false); } /** * @description Highlights the search keyword in file name elements. * @param {string} keyword - Lowercase keyword to highlight. */ #highlightKeyword(keyword) { const nameEls = this.list.querySelectorAll('.se-file-name-image:not(.se-file-name-back)'); for (let i = 0; i < nameEls.length; i++) { const el = nameEls[i]; const text = el.textContent; const idx = text.toLowerCase().indexOf(keyword); if (idx > -1) { el.innerHTML = text.substring(0, idx) + '<mark>' + text.substring(idx, idx + keyword.length) + '</mark>' + text.substring(idx + keyword.length); } } } /** * @param {MouseEvent} e - Event object */ #SideOpen(e) { const eventTarget = dom.query.getEventTarget(e); if (dom.utils.hasClass(eventTarget, 'active')) { dom.utils.removeClass(this.side, 'se-side-show'); dom.utils.removeClass(eventTarget, 'active'); } else { dom.utils.addClass(this.side, 'se-side-show'); dom.utils.addClass(eventTarget, 'active'); } } /** * @param {MouseEvent} e - Event object */ #SideClose({ target }) { if (target === this.sideOpenBtn) return; if (dom.utils.hasClass(this.sideOpenBtn, 'active')) { dom.utils.removeClass(this.side, 'se-side-show'); dom.utils.removeClass(this.sideOpenBtn, 'active'); } } } /** * @param {SunEditor.Deps} $ - Kernel dependencies * @param {boolean} useSearch - Whether to use the search function * @returns {{ html: HTMLElement, header: HTMLElement, titleArea: HTMLElement, tagArea: HTMLElement, body: HTMLElement, list: HTMLElement, side: HTMLElement, wrapper: HTMLElement, _loading: HTMLElement }} HTML */ function CreateHTMLInfos($, useSearch) { const lang = $.lang; const icons = $.icons; const htmlString = /*html*/ ` <div class="se-browser-content"> <div class="se-browser-header"> <button type="button" data-command="close" class="se-btn se-browser-close" class="close" title="${lang.close}" aria-label="${lang.close}"> ${icons.cancel} </button> <span class="se-browser-title"></span> </div> <div class="se-browser-wrapper"> <div class="se-browser-side"></div> <div class="se-browser-main"> <div class="se-browser-bar"> <div class="se-browser-search"> <button class="se-btn se-side-open-btn">${icons.side_menu_hamburger}</button> ${ useSearch ? /*html*/ ` <form class="se-browser-search-form"> <div class="se-browser-search-input-wrap"> <input type="text" class="se-input-form" placeholder="${lang.search}" aria-label="${lang.search}"> <button type="button" class="se-btn se-btn-plain se-browser-search-clear" title="${lang.cancel}" aria-label="${lang.cancel}" style="display:none">${icons.cancel}</button> </div> <button type="submit" class="se-btn" title="${lang.search}" aria-label="${lang.search}">${icons.search}</button> </form>` : '' } </div> </div> <div class="se-browser-body"> <div class="se-browser-tags"></div> <div class="se-loading-box sun-editor-common"><div class="se-loading-effect"></div></div> <div class="se-browser-menus"></div> <div class="se-browser-list"></div> </div> </div> </div> </div>`; const content = dom.utils.createElement('DIV', { class: 'se-browser-inner' }, htmlString); return { html: content, header: content.querySelector('.se-browser-header'), titleArea: content.querySelector('.se-browser-title'), tagArea: content.querySelector('.se-browser-tags'), body: content.querySelector('.se-browser-body'), list: content.querySelector('.se-browser-list'), side: content.querySelector('.se-browser-side'), wrapper: content.querySelector('.se-browser-wrapper'), _loading: content.querySelector('.se-loading-box'), }; } /** * @this {{ thumbnail: ((...args: *) => *), props: Array<*> }} * @description Define the HTML of the item to be put in `div.se-file-item-column`. * - Format: * - `[ { src: "image src", name: "name(@option)", alt: "image alt(@option)", tag: "tag name(@option)" } ]` * @param {BrowserFile} item Item of the response data's array */ function DrawItems(item) { const srcName = item.src.split('/').pop(); const thumbnail = item.thumbnail || ''; const src = thumbnail || item.src; const customProps = this.props?.map((v) => `data-${v}="${item[v]}"`).join(' ') || ''; const attrs = `data-type="${item.type}" data-command="${item.src}" data-name="${item.name || srcName}" data-thumbnail="${thumbnail}" data-extension="${item.src.split('.').pop()}" ${customProps}`; const props = `class="${thumbnail || 'se-browser-empty-image'}" src="${src}" alt="${item.alt || srcName}" ${attrs}`; return /*html*/ ` <div class="se-file-item-img"> ${this.thumbnail && !thumbnail && item.type !== 'image' ? `<div class="se-browser-empty-thumbnail" ${props}>${this.thumbnail(item)}</div>` : `<img class="${thumbnail || 'se-browser-empty-image'}" ${props}>`} <div class="se-file-name-image se-file-name-back"></div> <div class="se-file-name-image">${item.name || srcName}</div> </div>`; } export default Browser;