Supercharged Local Directory File Browser

Makes directory index pages (either local or remote open directories) actually useful. Adds sidebar and content preview pane; keyboard navigation; sorting; light/dark UI; preview images/fonts in navigable grids; browse subdirectories w/o page reload (“tree view”); media playback, shuffle/loop options; basic playlist (m3u, extm3u) & cuesheet (.cue) support; create, edit, preview, save markdown/plain text files; open font files, view complete glyph repertoire, save glyphs as .svg; more.

As of 2023-08-13. See the latest version.

  1. /* eslint-disable no-case-declarations, no-fallthrough, indent, no-mixed-spaces-and-tabs, no-multi-spaces, no-return-assign, no-useless-escape, quotes */
  2. /* jshint esversion: 6 */
  3.  
  4. // ==UserScript==
  5. // @name Supercharged Local Directory File Browser
  6. // @version 8.0.5
  7. // @description Makes directory index pages (either local or remote open directories) actually useful. Adds sidebar and content preview pane; keyboard navigation; sorting; light/dark UI; preview images/fonts in navigable grids; browse subdirectories w/o page reload (“tree view”); media playback, shuffle/loop options; basic playlist (m3u, extm3u) & cuesheet (.cue) support; create, edit, preview, save markdown/plain text files; open font files, view complete glyph repertoire, save glyphs as .svg; more.
  8. // @author gaspar_schot
  9. // @license GPL-3.0-or-later
  10. // @homepageURL https://openuserjs.org/scripts/gaspar_schot/Supercharged_Local_Directory_File_Browser
  11. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAgMAAAC+UIlYAAAACVBMVEUmRcmZzP8zmf8pVcWPAAAAAXRSTlMAQObYZgAAAFBJREFUeF7tyqERwDAMBEE3mX5UiqDmqwwziTPHjG7xrmzrLFtRaApDIRiKQlMYCsFQFJrCUAiGotAU5hTA1WB4fhkMBsOJwWAwgHvB8CHpBcTbpxy4RZNvAAAAAElFTkSuQmCC
  12. // @match file://*/*
  13. // @match https://www.example.com/path/to/directory/*
  14.  
  15. // @require https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js
  16. // @require https://cdn.jsdelivr.net/npm/markdown-it-footnote@3.0.2/dist/markdown-it-footnote.min.js
  17. // @require https://cdn.jsdelivr.net/npm/markdown-it-toc-done-right@2.1.0/dist/markdown-it-toc-made-right.min.js
  18. // @require https://cdn.jsdelivr.net/npm/markdown-it-sub@1.0.0/dist/markdown-it-sub.min.js
  19. // @require https://cdn.jsdelivr.net/npm/markdown-it-sup@1.0.0/dist/markdown-it-sup.min.js
  20. // @require https://cdn.jsdelivr.net/npm/markdown-it-deflist@2.0.3/dist/markdown-it-deflist.min.js
  21. // @require https://cdn.jsdelivr.net/npm/markdown-it-multimd-table@4.0.2/dist/markdown-it-multimd-table.min.js
  22. // @require https://cdn.jsdelivr.net/npm/markdown-it-center-text@1.0.4/dist/markdown-it-center-text.min.js
  23. // @require https://cdn.jsdelivr.net/npm/opentype.js@1.3.4/dist/opentype.min.js
  24. // @require https://cdn.jsdelivr.net/npm/@gerhobbelt/markdown-it-checkbox@1.2.0-3/dist/markdownItCheckbox.umd.js
  25.  
  26.  
  27. // @namespace https://greatest.deepsurf.us/users/16170
  28. // ==/UserScript==
  29.  
  30. (function() {
  31. // ************ J + M + J ************* //
  32. 'use strict';
  33. // ***** UI SETTINGS ***** //
  34. const UI_Prefs_Bool = {
  35. alternate_background: true, apps_as_dirs: true, autoload_index_files: true, ignore_ignored_items: true, media_autoload: true, media_autoplay: true, media_play_all: true,
  36. show_details: true, show_ignored_items: true, show_image_thumbnails: true, show_image_thumbnails_always: true, show_large_image_thumbnails: true, show_invisibles: true, show_numbers: true,
  37. show_sidebar: true, text_editing_enable: true, texteditor_split_view: true, texteditor_sync_scroll: true, use_custom_icons: true
  38. }
  39. const UI_Prefs_Non_Bool = {
  40. grid_font_size: 1, // Default = 1
  41. grid_image_size: 184, // Default = 184 (200px - 16px)
  42. sort_by: 'default', // Choose from: 'name', 'size', 'date', 'kind', 'ext', 'default ( = Chrome sorting: dirs on top, files alphabetical).
  43. sort_direction: 'ascending', // Choose from: 'ascending' (A-Z) [default] or 'descending' (Z-A).
  44. texteditor_view: 'styled', // Options: 'raw','styled','html'
  45. theme: 'light', // Options: 'light' or 'dark'
  46. ui_font: 'system-ui, sans-serif', // Choose an installed font for the UI; if undefined, use browser defaults instead. [system-ui, sans-serif]
  47. ui_font_size: '13px', // Choose a default UI font size; use any standard CSS units.
  48. ui_scale: '100',
  49. }
  50. let UI_Settings = {...UI_Prefs_Bool, ...UI_Prefs_Non_Bool};
  51. const Item_Kinds = {
  52. dir: ['/'], // loaded in iframe#content_iframe
  53. app: ['app/','app','appimage','apk','exe','ipa','ipk','jar','msi','wsf'], // generally ignored; some apps may be opened as directories
  54. alias: ['alias','desktop','directory','lnk','symlink','symlink/'],
  55. archive: ['7z','archive','b6z','bin','bzip','bz2','cbr','dmg','gz','iso','mpkg','pkg','rar','sit','sitx','tar','tar.gz','zip','zipx','zxp'], // ignored
  56. audio: ['aac','aif','aiff','ape','flac','m4a','mka','mp3','ogg','opus','wav'], // loaded in audio#audio
  57. bin: ['a','ase','bundle','dll','dyld','dylib','gem','icc','msi','profraw','pyc','pyo','o','rakefile','ri','so','torrent','xml','2','opml','qm','scpt','uo','vsix','zwc'], // ignored
  58. code: ['bak','bash','bash_profile','bashrc','bat','cgi','com','c','cfg','cnf','codes','coffee','conf','csh','cshrc','cson','css','cuetxt','custom_aliases','d','default','description','dist','editorconfig', 'emacs','example','gemspec','gitconfig','gitignore','gitignore_global','h','hd','ini','js','json','jsx','less','list','local','login','logout','lua','mkshrc','old','pc','php','pl','plist','pre-oh-my-zsh','profile','pth','py','rb','rc','rdoc','sass','settings','sh','strings','taskrc','tcl','viminfo','vimrc','vue','vtt','yaml','yml','zlogin','zlogout','zpreztorc','zprofile','zsh','zshenv','zshrc'], // treated as text, opened in iframe#content_iframe text editor
  59. database: ['accdb','db','dbf','mdb','pdb','sql', 'sqlite','sqlitedb','sqlite3'], // ignored
  60. ebook: ['azw','azw1','azw3','azw4','epub','ibook','kfx','mobi','tpz'], // ignored
  61. font: ['otf','ttf','woff','woff2','afm','pfb','pfm','tfm'], // opened in div#content_font
  62. graphics: ['afdesign','afpub','ai','book','dtp','eps','fm','icml','icns','idml','indd','indt','inx','mif','pmd','pub','qxb','qxd','qxp','sla','swf','ai','arw','cr2','dng','eps','jpf','nef','psd','psd','raw', 'tif','tiff'], // ignored
  63. htm: ['htm','html','xhtm','xhtml'], // opened in iframe#content_iframe
  64. image: ['apng','bmp','gif','ico','jpeg','jpg','png','svg','webp'],
  65. link: ['url','webloc','inetloc'],
  66. markdown: ['md','markdown','mdown','mkdn','mkd','mdwn','mdtxt','mdtext'], // treated as text, opened in iframe#content_iframe text editor
  67. other_ignored: ['alias','cue','dat','dic','idx','xmp'],
  68. office: ['csv','doc','docx','key','numbers','odf','ods','odt','pages','rtf','scriv','wpd','wps','xlr','xls','xlsx','xlm'], // ignored
  69. playlist: ['m3u','m3u8','pls','asx','wpl','xspf'],
  70. pdf: ['pdf'], // open in #content_pdf
  71. system: ['DS_Store','ds_store','icon','ics','spotlight-v100/','temporaryitems/','documentrevisions-v100/','trashes/','fseventsd/','dbfseventsd','file','programdata','localized'], // ignored system items
  72. text: ['log','nfo','txt','text','readme'], // opened in iframe#content_iframe text editor
  73. video: ['m4v','mkv','mov','mp4','mpeg','webm'] // loaded in video#content_video
  74. };
  75. const Item_Settings = { // ITEM_SETTINGS: Ignore or Exclude files by extension (prevents browser from attempting to download the file).
  76. ignored: [...Item_Kinds.archive,...Item_Kinds.bin,...Item_Kinds.database,...Item_Kinds.graphics,...Item_Kinds.other_ignored,...Item_Kinds.office,...Item_Kinds.playlist,...Item_Kinds.system]
  77. };
  78.  
  79. // ***** UTILITIES ***** //
  80. function loadFileURL() { // ===> LOAD FILE URL
  81. // if window.location points to a file, change the location to the file's container dir, add search_param of file name; then load the file's container dir and load file in content pane.
  82. let search_params = searchParamsGet();
  83. search_params.set( 'file', window.location.pathname.split('/').reverse()[0]);
  84. window.location = window.location.pathname.slice( 0,window.location.pathname.lastIndexOf('/') ) +'/?'+ search_params ;
  85. return;
  86. }
  87. if ( !window.location.pathname.endsWith('/') && window.top === window.self ) { loadFileURL(); } // load file urls
  88. //==============================//
  89. function isTopWindow() { return ( window.top === window.self || false ) } // ===> TOP WINDOW OR IFRAME
  90. function getBrowser() { //*** needs testing for new userAgentData object --> what are possible brand names?; combine with getOS() // ===> GET BROWSER
  91. let brand = ( navigator.userAgentData !== undefined ? navigator.userAgentData.brands[1].brand.toLowerCase() : navigator.userAgent );
  92. switch(true) {
  93. case brand === 'chromium' || ( /chrome?chromium/.test(brand) ): return 'is_chrome';
  94. case brand === 'msie' || ( /msie/.test(brand) ): return 'is_explorer';
  95. // case brand === 'edge' || ( /edge/.test(brand) ): return 'is_edge'; // need case for ms edge
  96. case brand === 'opera' || ( /opera/.test(brand) ): return 'is_opera';
  97. case brand === 'safari' || ( /safari/.test(brand) ): return 'is_safari';
  98. case brand === 'firefox' || ( !/chrome|chromium/.test(brand) ): return 'is_gecko';
  99. }
  100. }
  101. function getOS() { // modded from https://***stackoverflow.com/questions/38241480/detect-macos-ios-windows-android-and-linux-os-with-js // ===> GET OS
  102. let platform = ( navigator.userAgentData !== undefined ? navigator.userAgentData.platform : window.navigator.platform ).toLowerCase();
  103. let macos_platforms = ['macos','macintosh','macintel','macppc','mac68k'], windows_platforms = ['win32','win64','windows','wince'], os = null;
  104. switch(true) {
  105. case macos_platforms.indexOf(platform) !== -1: os = 'macos'; break;
  106. case windows_platforms.indexOf(platform) !== -1: os = 'windows'; break;
  107. // case iosPlatforms.indexOf(platform) !== -1: os = 'ios'; break; // just in case;
  108. // case /Android/.test(userAgent): os = 'android'; break; // just in case;
  109. case !os && /Linux/.test(platform): os = 'linux'; break;
  110. }
  111. return os;
  112. }
  113. function newURL(link) { try { return new URL(link,document.baseURI); } catch( error ) { return new URL(encodeURI(link) ); } } // ===> NEW URL
  114. function decodeURIComponentSafe(str) { if ( !str ) { return str; } // ===> DECODE URI COMPONENT SAFE; // Fix "%" error in file name; see stackoverflow.com/questions/7449588/why-does-decodeuricomponent-lock-up-my-browser
  115. try { return decodeURIComponent(str.replace(/%(?![0-9a-fA-F]{2})/g,'%25') ).replace(/\"/g,'\"'); } catch(e) { return str; } // replace % with %25 if not followed by two a-f/number; replace " with html entity
  116. }
  117. //function sanitizeReservedChars(str) { let chars = ['#','$','&','+',',','/',':',';','=','?','@','[',']']; }
  118. //function escapeStr(str) { str = str.replace(/([$?*+()[]|^])/g,'\\$1'); return str; } // ===> ESCAPE STRING
  119. function convertHex2Decimal(d) { return parseInt(d, 16); }
  120. function convertDecimal2Hex(d, padding) { let hex = Number(d).toString(16); hex = ( isNaN(hex) ? null : "000000".substr(0, padding - hex.length) + hex ); return hex; }
  121. //==============================//
  122. const window_protocol = window.location.protocol; // GLOBAL: protocol
  123. //const window_origin = window_protocol +'//'+ window.location.host; // GLOBAL: origin
  124. let window_location = decodeURIComponentSafe( [location.protocol, '//', location.host, location.pathname].join('') ); // GLOBAL: current location
  125. const current_dir_path = window_location.replace(/([/|_|—])/g,'$1<wbr>').replace(/\\/g,'/'); // GLOBAL: current dir path w/o query string for display
  126. // const current_dir = window_location.split('/').slice(-2,-1).toString(); // GLOBAL: current dir
  127. //==============================//
  128. function setLocation(link) { window.location = link; }
  129. function changeLocation(args) { // args[0] === href, args[1] === 'external || ok'
  130. switch(true) {
  131. case args[1] === 'external': window.open(args[0]); break; // open external menu links: about, coffee, contact
  132. case args[1] === 'ok': window.location = args[0]; break;
  133. case ( /has_\w+list/.test(getClassNames('body'))): args = window.location.href; // nobreak; show playlist warning
  134. default: showWarning( 'setLocation',args.toString() );
  135. }
  136. }
  137. function searchParamsGet() { let search_params = new URL(window.location).searchParams; search_params.sort(); return search_params; } // ===> GET SEARCH PARAMS
  138. function searchParamSet(key,value,bool) { let search_params = searchParamsGet(); search_params.set( key, value ); if ( bool !== false ) { updateSearchParams(search_params);} } // ===> SET SEARCH PARAM (bool false = don't update)
  139. function searchParamDelete(key) { let search_params = searchParamsGet(); search_params.delete(key); updateSearchParams(search_params); } // ===> REMOVE SEARCH PARAM
  140. function updateSearchParams(search_params) { // ===> UPDATE SEARCH PARAMS
  141. search_params = sanitizeSearchParams(search_params); search_params.sort(); // sort and sanitize params
  142. let search_params_str = search_params.toString().replace(/%2F/g,'').replace('/','').replace(/%2Cfalse/g,''); // further sanitization
  143. let new_location = ( search_params_str.length === 0 ? window.location.pathname : window.location.pathname +'?'+ search_params_str ); // don't add ? if no search params
  144. window.history.replaceState({}, document.title, new_location); // set new location
  145. if ( isTopWindow() ) { updateParentLinks(); }
  146. }
  147. function sanitizeSearchParams(search_params_str) { // remove search_params that are not in UI_Settings
  148. for ( let entry of search_params_str.entries() ) { if ( !UI_Settings[entry[0]] && !/selected|history|width/.test(entry[0]) ) { search_params_str.delete(entry[0]); } } return search_params_str;
  149. }
  150. function getCurrentUIPref(pref_id) { // ===> GET SEARCH PARAM value by key
  151. let search_params = searchParamsGet(), value = '';
  152. switch(true) {
  153. case pref_id === 'width': if ( !isTopWindow() ) { return; } // width: set the stored sidebar width or use default 30%
  154. value = ( !search_params.has(pref_id) || window.innerWidth === 0 ? 30 : Math.round(100 * Number.parseInt(search_params.get('width'))/window.innerWidth) ); break; // percentage
  155. case pref_id === 'parent_id':
  156. value = ( search_params.has(pref_id) ? search_params.get(pref_id) : UI_Settings?.[pref_id] ? UI_Settings[pref_id].toString() : '' ); break; //
  157. default: // if query_string has key/value pair, use it, else use key/value pair from UI_Settings
  158. value = ( search_params.has(pref_id) ? search_params.get(pref_id) : UI_Settings?.[pref_id] ? UI_Settings[pref_id].toString() : pref_id ).toString();
  159. value = ( value.replace('%2F','').replace('/','') ?? '' ); // some servers add a '/' to end of query string
  160. } return value;
  161. }
  162. function getNewUIPref(key) {
  163. let value, bool_prefs = Object.keys(UI_Prefs_Bool);
  164. let non_bool_prefs = {
  165. 'sort_direction_ascending': {'sort_direction':'descending'}, 'sort_direction_descending': {'sort_direction':'ascending'}, 'sort_by_name': {'sort_by':'name'},
  166. 'sort_by_default': {'sort_by':'default'}, 'sort_by_duration': {'sort_by':'duration'}, 'sort_by_size': {'sort_by':'size'},
  167. 'sort_by_date': {'sort_by':'date'}, 'sort_by_kind': {'sort_by':'kind'}, 'sort_by_ext': {'sort_by':'ext'},
  168. 'texteditor_view': {'texteditor_view':getCurrentUIPref('texteditor_view')},
  169. 'texteditor_view_raw': {'texteditor_view':'raw'}, 'texteditor_view_styled': {'texteditor_view':'styled'}, 'texteditor_view_html': {'texteditor_view':'html'},
  170. 'theme': {'theme':(getCurrentUIPref('theme') === 'light' ? 'dark' : 'light') },
  171. 'theme_dark': {'theme':'dark'}, 'theme_light': {'theme':'light'},
  172. 'ui_font': {'ui_font':getCurrentUIPref('ui_font')},'ui_scale': {'ui_scale':getCurrentUIPref('ui_scale')}
  173. }
  174. switch(true) {
  175. case bool_prefs.includes(key): return ( getCurrentUIPref(key) === 'true' ? [key,'false'] : [key,'true']); // toggle bool prefs
  176. default: value = Object.values( non_bool_prefs[key] ).toString(); key = Object.keys(non_bool_prefs[key]).toString(); return [key,value]; // get value for key; then key (i.e., don't redefine key before getting value)
  177. }
  178. }
  179. let str = ''; // global str var;
  180. function timeoutID() { return window.setTimeout( () => { str = ''; }, 1000 ); } // ===> TIMEOUT ID: reset typed string to '' after 1.5 sec.
  181. //==============================//
  182. function getEl(sel) { try { return document.querySelector(sel); } catch (error) { return null; } }
  183. function getEls(sel) { try { return document.querySelectorAll(sel); } catch (error) { return null; } }
  184. function elExists(sel) { return ( document.querySelector(sel) !== null ? true : false ); }
  185. function fileNotFound(e,id) {
  186. if ( e.type === 'error') { if (id === 'audio') { addClass('#content_pane','has_audio_error'); setContentTitle('has_audio_error'); } else { addClass('#content_pane','content_error'); closeContent(); setContentTitle('error'); } }
  187. }
  188. function getVisibleElsBySelector(sel) { // ===> GET VISIBLE ELS BY SELECTOR // remove els with display:none or 0 width/height
  189. let els = Array.from( getEls(sel) ).filter( (el) => { let el_styles = window.getComputedStyle(el); return ( el_styles.getPropertyValue('display') !== 'none' || ( el.offsetWidth > 0 || el.offsetHeight > 0 ) ); });
  190. return els;
  191. }
  192. function getContentPaneData() { return getAttr('#content_pane','data-content'); } // ===> GET CONTENT_PANE DATA content
  193. function hasContent(args) { // ===> HAS CONTENT?
  194. switch(true) {
  195. case args === undefined: return ( hasClass('#content_pane','has_audio') || getContentPaneData() !== 'has_null' ? true : false ); // has any content
  196. case args.includes('audio'): return ( hasClass('#content_pane','has_audio') && (args.includes('ignore') || getContentPaneData() === 'has_null') ? true : false ); // has audio only or ignore other content
  197. case args.includes('_'): return hasClass('#content_pane','has_'+args);
  198. default: return ( args.split(',').includes( getContentPaneData()?.split('_')[1] ) ? true : false ); // has named content (e.g., pdf, iframe)
  199. }
  200. }
  201. function initContentError(id,content_el_id) { if ( id !== 'close' ) { getEl(content_el_id).addEventListener('error',(e) => { fileNotFound(e,content_el_id); }); } } // ===> INIT CONTENT ERROR
  202. //==============================//
  203. function getClassNames(sel) { return getEl(sel)?.className; }
  204. function hasClass(sel,classname) { let el = getEl(sel); return el?.classList.contains(classname); } // ===> HAS CLASS
  205. function addClass(sel,classname) { let els = Array.from(getEls(sel)), classes = classname.split(' ').filter(item => item); els?.forEach( el => el.classList.add(...classes) ); } // ===> ADD CLASS
  206. function removeClass(sel,classname) { // ===> REMOVE CLASS
  207. let els = Array.from(getEls(sel)), classes = ( classname?.split(' ')?.filter(item => item) || null );
  208. if ( classname === undefined || classes === null ) { els?.forEach( el => el.removeAttribute('class') ); } else { els?.forEach( el => el.classList.remove(...classes) ); } // if no className, remove all classes
  209. }
  210. function addRemoveClassSiblings(sel,classname) { // ===> ADD/REMOVE CLASS SIBLINGS
  211. let el = document.querySelector(sel), siblings = el?.parentElement.children;
  212. if ( el !== null ) { Array.from(siblings).forEach( sibling => sibling.classList.remove(...classname.replace(/\s{2,}/g,' ').split(' ') ) ); addClass(sel,classname); } // remove class from els & add class to selected el
  213. }
  214. //==============================//
  215. function getAttr(sel,attributeName) { return getEl(sel)?.getAttribute(attributeName); } // ===> GET ATTRIBUTE
  216. function hasAttr(sel,attributeName) { return getEl(sel)?.hasAttribute(attributeName); } // ===> HAS ATTRIBUTE
  217. function setAttr(sel,attributeName,value) { getEl(sel)?.setAttribute(attributeName,value); } // ===> SET ATTRIBUTE
  218. function removeAttr(sel,attributeNamesArr) { // ===> REMOVE ATTRIBUTE
  219. if ( typeof attributeNamesArr === 'string' ) { attributeNamesArr = [attributeNamesArr]; }
  220. let els = getEls(sel); Array.from(els)?.forEach( el => attributeNamesArr.forEach( attributeName => el.removeAttribute(attributeName) ) );
  221. }
  222. function getData(sel,keyname) { let el = ( typeof sel === 'string' ? getEl(sel) : sel ); return el?.getAttribute('data-'+keyname); } // ===> GET DATASET
  223. function setData(sel,keyname,value) { if ( elExists(sel) ) { setAttr(sel,'data-'+keyname, value); } } // ===> SET DATASET
  224. function deleteData(sel,keyname) { removeAttr(sel,'data-'+keyname); } // ===> REMOVE DATASET
  225. function setStyle(sel,property,value) { getEl(sel)?.style.setProperty(property,value); } // ===> SET STYLE
  226. function setValue(sel,value) { getEl(sel).value = value; } // ===> SET VALUE
  227. //==============================//
  228. function clickThis(sel) { let el = getEl(sel); ( el?.querySelector('a')?.click() || el?.click() ) } // ===> CLICK THIS by CSS selector
  229. // function dblclick(el,func) { var evt = new MouseEvent('dblclick'); el.addEventListener(evt,func); el.dispatchEvent(evt); } // ===> DOUBLE CLICK
  230. function altKey(e) { return ( !e.metaKey && !e.ctrlKey && e.altKey && !e.shiftKey ); } // ===> ALT KEY test
  231. function altShiftKey(e) { return ( !e.metaKey && !e.ctrlKey && e.altKey && e.shiftKey ); } // ===> ALT SHIFT KEY test
  232. function cmdKey(e) { return ( (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey ); } // ===> CMD/CTRL KEY test
  233. function cmdAltKey(e) { return ( (e.metaKey || e.ctrlKey) && e.altKey && !e.shiftKey ); } // ===> CMD/CTRL ALT KEY test
  234. function cmdShiftKey(e) { return ( (e.metaKey || e.ctrlKey) && !e.altKey && e.shiftKey ); } // ===> CMD/CTRL SHIFT KEY test
  235. // function cmdAltShiftKey(e) { return ( (e.metaKey || e.ctrlKey) && e.altKey && e.shiftKey; } // ===> CMD/CTRL ALT SHIFT KEY test (not used)
  236. function eStopPrevent(e) { e?.preventDefault(); e?.stopPropagation(); }
  237. //============================//
  238. // ***** BASIC UI FUNCTIONS ***** //
  239. function isInViewport(sel) { const rect = ( getEl(sel) !== null ? getEl(sel).getBoundingClientRect() : null ); if ( rect === null ) { return false; }
  240. return ( rect.top >= getEl('#sidebar_header').offsetHeight && rect.bottom <= (window.innerHeight - getEl('#sidebar_footer').offsetHeight || document.documentElement.clientHeight - getEl('#sidebar_footer').offsetHeight) );
  241. }
  242. function scrollThis(container_ID, sel, bool) { // ===> SCROLL to Selected Item
  243. let container = getEl(container_ID);
  244. if ( container?.height === 0 || isInViewport(sel) ) { return; } // don't scroll hidden elements
  245. let scroll_el = container?.querySelector(sel), scroll_behavior = ( ( bool !== undefined || bool === true ) ? 'instant' : 'smooth' ), scroll_block = ( hasClass('body','is_gecko') ? 'start' : 'nearest' );
  246. scroll_el?.scrollIntoView({ behavior:scroll_behavior, block:scroll_block, inline:'nearest' });
  247. }
  248. function mouseMove(e,sel,startX,startY,elOffsetLeft,elOffsetTop) { // ===> Init events to allow glyphs to be dragged into view
  249. let scale_factor = ( sel === '#font_specimen_glyph' ? 2 : 1 ); // scale_factor needed for svg glyphs
  250. setStyle(sel,'left',elOffsetLeft + (e.pageX - startX)*scale_factor + 'px'); setStyle(sel,'top',elOffsetTop + (e.pageY - startY)*scale_factor +'px');
  251. }
  252. //==============================// OPEN/SAVE FILES
  253. function openFile(args) { menuClose(); // ===> OPEN FILE; type: font or playlist.
  254. if ( window.File && window.FileReader && window.FileList && window.Blob ) { // if browser supports file API...
  255. let files = args[0].target.files[0], id = args[1], reader = new FileReader();
  256. switch(id) { case 'open_font': reader.readAsArrayBuffer(files); break; case 'open_playlist': reader.readAsText(files); break; } // get the file reader
  257. reader.onload = () => { // on file reader load
  258. switch(true) {
  259. case id === 'open_font': openFontFile(files,reader); break;
  260. case id === 'open_playlist': openPlaylist(files.name,'',reader.result); break;
  261. } // open the file
  262. return true;
  263. }
  264. getEl('#'+id).value = ''; // reset input to allow same item to be reopened immediately after closing
  265. } else { alert('Can\'t open file: file APIs are not fully supported in this browser.'); } // else error
  266. }
  267. function saveFile(content,mimetype,file_name) { // ===> SAVE FILE
  268. let blob = new Blob([content], {type: mimetype});
  269. let download_el = window.document.createElement('a'); download_el.style = "display:none"; download_el.href = window.URL.createObjectURL(blob); download_el.download = file_name; // define & style download_el
  270. document.body.appendChild(download_el); download_el.click(); document.body.removeChild(download_el); URL.revokeObjectURL(blob); // add download_el, click, & remove
  271. }
  272. // END UTILITIES
  273. //==============================//
  274. // ***** SET UP UI ELEMENTS ***** //
  275. function updateParentLinkSearchParams(str) { //*** decrement selected and history values ***// // ===> UPDATE PARENT LINK SEARCH PARAMS
  276. let query_str = new URLSearchParams(str); query_str.sort(); // make new search params from window.location.search
  277. let history = ( query_str.has('history') ? query_str.get('history') : undefined );
  278. switch(true) {
  279. case history !== undefined:
  280. history = history.split(' ');
  281. switch(true) {
  282. case history.length > 1: query_str.set('selected',history[0]); history.shift(); query_str.set('history',history.join('+')); break;
  283. case history.length === 1: query_str.set('selected',history[0]); history.shift(); query_str.delete('history'); break;
  284. }
  285. break;
  286. default: query_str.delete('selected');
  287. }
  288. return decodeURIComponentSafe(query_str.toString());
  289. }
  290. function createParentLinks() { // ===> CREATE PARENT LINKS
  291. let link, links = [], search_params = searchParamsGet(); search_params.sort();
  292. let query_str = search_params.toString();
  293. let link_pieces = window_location.split('/'); link_pieces = link_pieces.slice(2,-2); // make array of parent directories; remove beginning and ending empty elements and current directory
  294. while ( link_pieces.length > 0 ) { // while there are link pieces...
  295. query_str = updateParentLinkSearchParams(query_str); // update selected and history
  296. link = window_protocol +'//'+ link_pieces.join('/') + '/?' + query_str; links.push(link); link_pieces.pop(); // assemble link; add to link array; remove last link piece and repeat...
  297. }
  298. return links;
  299. }
  300. function createParentLinkItems() { // ===> CREATE PARENT LINK ITEMS
  301. let parent_link_menu_items = [], links = createParentLinks();
  302. for ( let i = 0; i < links.length; i++ ) {
  303. let display_name = links[i].split('/?')[0]; display_name = display_name.replace(/\//g,'\/<wbr>');
  304. let menu_item = `<li><a id="parents_link_${i}" href="${ links[i] }" class="display_block padding_4_8">${ display_name }/</a></li>`;
  305. parent_link_menu_items.push(menu_item);
  306. }
  307. let parent_link = ( links[0] === undefined ? window.location.href : links[0]); parent_link = parent_link.replace(/parents_link_/,'parent_link_');
  308. return [parent_link_menu_items.join(''),parent_link]; // return parents link items
  309. }
  310. function updateParentLinks() { // ===> UPDATE PARENT LINKS and init new item events
  311. let links = createParentLinkItems(); getEl('#parents_links').innerHTML = links[0]; getEl('#parent_dir_nav a').href = links[1]; // add the links
  312. getEls('#sidebar_menu_parent a,#parents_links a').forEach( el => el.onclick = function(e) { eStopPrevent(e); showWarning('changeLocation',[this.href,'false']); }); // reinit onclick
  313. }
  314. //==============================//
  315. // SVG UI ICONS
  316. const SVG_UI_Icons = {
  317. 'arrow': '<svg viewBox=\'0 0 20 20\' xmlns=\'http://www.w3.org/2000/svg\' class=\'invert\'><path fill=\'%23888\' fill-opacity=\'.75\' d=\'m4 4 12 6-12 6z\'/></svg>',
  318. 'bookmark': '<svg viewBox=\'0 0 20 20\' xmlns=\'http://www.w3.org/2000/svg\' class=\'invert\'><path fill=\'%23888\' d=\'m2 2c0-1.1.9-2 2-2h12a2 2 0 0 1 2 2v18l-8-4-8 4zm2 0v15l6-3 6 3v-15z\'/></svg>',
  319. 'check_mark': '<svg viewBox=\'0 0 12 9\' xmlns=\'http://www.w3.org/2000/svg\'><path fill=\'currentColor\' d=\'m-.071 10.929 2.571-2.571 4.5 4.499 10.285-10.285 2.571 2.572-12.856 12.856z\' transform=\'matrix(.55 0 0 .55 .578932 -1.01245)\'/></svg>',
  320. 'chevron': '<svg viewBox=\'0 0 24 14\' id=\'svg_chevron\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0v14h3v-11h11v-3z\' transform=\'matrix(.707107 .707107 -.707107 .707107 11.8995 1)\'/></svg>',
  321. 'document': '<svg viewBox=\'0 0 20 20\' xmlns=\'http://www.w3.org/2000/svg\'><path fill=\'%23222222\' d=\'M4 18h12V6h-4V2H4v16zm-2 1V0h12l4 4v16H2v-1z\' /></svg>',
  322. 'error': '<svg viewBox=\'0 0 20 20\' xmlns=\'http://www.w3.org/2000/svg\'><g fill-opacity=\'.75\'><path d=\'m1.075 18.05 8.146-16.683c.236-.484.924-.491 1.169-.011l8.537 16.683c.223.435-.093.952-.582.952h-16.683c-.483 0-.799-.507-.587-.941z\' fill=\'%23ffb636\' /><path d=\'m11.055 7.131-.447 6.003c-.034.45-.425.787-.874.753-.408-.03-.724-.356-.753-.753l-.447-6.003c-.052-.696.47-1.302 1.167-1.354.696-.052 1.302.47 1.354 1.166.005.061.004.129 0 .188zm-1.26 8.037c-.641 0-1.159.518-1.159 1.158 0 .641.518 1.159 1.159 1.159.64 0 1.158-.518 1.158-1.159 0-.64-.518-1.158-1.158-1.158z\'/></g></svg>',
  323. 'external_link': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'M10.443,2.5l-3.439,-0l0,-1.5l5.996,0l0,6.02l-1.5,0l0,-3.455l-5.716,5.715l-1.06,-1.06l5.719,-5.72Zm1.057,5.5l-0,5l-10.5,0l-0,-10.5l5,0l-0,1.5l-3.5,0l-0,7.5l7.5,0l-0,-3.5l1.5,0Z\' style=\'fill:%23888;\'/></svg>',
  324. 'folder': '<svg viewBox=\'0 0 20 20\'><path fill=\'%23222\' d=\'m0 4c0-1.1.9-2 2-2h7l2 2h7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2-2zm2 2v10h16v-10z\'/></svg>',
  325. 'grid': '<svg viewBox=\'0 0 20 20\'><path fill=\'currentColor\' d=\'M0 0h9v9H0V0zm2 2v5h5V2H2zm-2 9h9v9H0v-9zm2 2v5h5v-5H2zm9-13h9v9h-9V0zm2 2v5h5V2h-5zm-2 9h9v9h-9v-9zm2 2v5h5v-5h-5z\' /></svg>',
  326. 'menu': '<svg viewBox=\'0 0 13 10\'><g fill=\'%23222\'><path d=\'m0 0h13v2h-13z\'/><path d=\'m0 4h13v2h-13z\'/><path d=\'m0 8h13v2h-13z\'/></g></svg>',
  327. 'minus': '<svg viewBox=\'0 0 20 20\'><path fill=\'%23222\' d=\'m1 8h18v4h-18z\'/></svg>',
  328. 'multiply': '<svg viewBox=\'0 0 20 20\' id=\'svg_multiply\'><path fill=\'%23222\' d=\'m10 7 6-6 3 3-6 6 6 6-3 3-6-6-6 6-3-3 6-6-6-6 3-3z\'/></svg>',
  329. 'music': '<svg width=\'100%\' height=\'100%\' viewBox=\'0 0 84 84\' version=\'1.1\' xmlns=\'http://www.w3.org/2000/svg\' xml:space=\'preserve\' style=\'fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;\'><path d=\'M66.613,58.258c-0,3.775 -1.721,6.934 -5.163,9.475c-3.154,2.309 -6.683,3.463 -10.588,3.463c-2.283,-0 -4.158,-0.538 -5.616,-1.617c-1.621,-1.229 -2.429,-2.95 -2.429,-5.158c-0,-3.492 1.658,-6.558 4.966,-9.204c3.134,-2.488 6.496,-3.734 10.088,-3.734c3.029,0 5.237,0.605 6.625,1.809l-0,-39.538l-28.146,7.584l-0,44.891c-0,3.775 -1.721,6.929 -5.158,9.471c-3.159,2.313 -6.688,3.467 -10.588,3.467c-2.287,-0 -4.158,-0.542 -5.625,-1.617c-1.617,-1.233 -2.425,-2.954 -2.425,-5.158c0,-3.492 1.658,-6.559 4.967,-9.204c3.133,-2.488 6.496,-3.734 10.087,-3.734c3.029,0 5.238,0.604 6.621,1.809l0,-48.355l32.383,-8.741l0.001,54.091Z\' style=\'fill:%23888;fill-opacity:0.4;fill-rule:nonzero;\'/></svg>',
  330. 'plus': '<svg viewBox=\'0 0 20 20\'><path fill=\'%23222\' d=\'m8.001 1h3.999v7h7v4h-7l-.001 7h-3.999v-7h-7v-4h7z\'/></svg>',
  331. 'prev_next_track': '<svg viewBox=\'0 0 20 20\'><path fill=\'%23222\' d=\'m13 5h2v10h-2zm-8 0 8 5-8 5z\'/></svg>',
  332. 'spinner': '<svg viewBox=\'0 0 100 100\' class=\'display_none invert\' preserveAspectRatio=\'xMidYMid\' width=\'32\' height=\'32\' xmlns=\'http://www.w3.org/2000/svg\' id=\'loading_spinner\'><animateTransform attributeName=\'transform\' type=\'rotate\' values=\'0;45\' keyTimes=\'0;1\' dur=\'0.25s\' repeatCount=\'indefinite\'/><path fill=\'%23000\' fill-opacity=\'.66\' d=\'m29.49-5.5h8v11h-8a30 30 0 0 1 -4.75 11.46l5.66 5.66-7.78 7.78-5.66-5.66a30 30 0 0 1 -11.46 4.75v8h-11v-8a30 30 0 0 1 -11.46-4.75l-5.66 5.66-7.78-7.78 5.66-5.66a30 30 0 0 1 -4.75-11.46h-8v-11h8a30 30 0 0 1 4.75-11.46l-5.66-5.66 7.78-7.78 5.66 5.66a30 30 0 0 1 11.46-4.75v-8h11v8a30 30 0 0 1 11.46 4.75l5.66-5.66 7.78 7.78-5.66 5.66a30 30 0 0 1 4.75 11.46m-29.49-14.5a20 20 0 1 0 0 40 20 20 0 1 0 0-40\' transform=\'matrix(.7189408 .69507131 -.69507131 .7189408 50 50)\'/></svg>',
  333. 'toggle': '<svg viewBox=\'0 0 20 20\'><g fill=\'%23222\'><path d=\'m10.207 9.293-.707.707 5.657 5.657 1.414-1.414-4.242-4.243 4.242-4.243-1.414-1.414z\'/><path d=\'m4.207 9.293-.707.707 5.657 5.657 1.414-1.414-4.242-4.243 4.242-4.243-1.414-1.414z\'/></g></svg>',
  334. 'ui_layout': '<svg width=\'100px\' height=\'100%\' viewBox=\'0 0 50 39\' version=\'1.1\' xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\' xml:space=\'preserve\' xmlns:serif=\'http://www.serif.com/\' style=\'fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;\'><rect x=\'0.5\' y=\'0.5\' width=\'14.5\' height=\'38\' style=\'fill:rgb(204,204,204);\'/><path d=\'M50,0L50,39L0,39L0,0L50,0ZM49,1L1,1L1,38L49,38L49,1Z\'/><path d=\'M15,0.5L15,38.5\' style=\'fill:none;stroke:black;stroke-width:1px;\'/><g transform=\'matrix(1,0,0,1,0,1.5)\'><path d=\'M0.5,5L15,5\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,15.0936,-0.5)\'><path d=\'M0,5L34.406,5\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g><g transform=\'matrix(1,0,0,1,0,1.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,1.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,3.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,5.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,7.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,9.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,11.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,13.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,15.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,17.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,19.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,21.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,23.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,25.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,27.5)\'><path d=\'M2.5,7L3,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><path d=\'M0.5,36.5L15,36.5\' style=\'fill:none;stroke:black;stroke-width:1px;\'/><g transform=\'matrix(1,0,0,1,0,1.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,3.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,5.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,7.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,9.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,11.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,13.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,15.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,17.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,19.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,21.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,23.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,25.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g><g transform=\'matrix(1,0,0,1,0,27.5)\'><path d=\'M5,7L13,7\' style=\'fill:none;stroke:black;stroke-width:1px;\'/></g></g><g class=\'invert\'><g transform=\'matrix(1.42857,0,0,1.42857,22.5,10.5)\'><path d=\'M0.369,9.141C0.117,8.463 0,7.745 0,7C0,3.137 3.137,0 7,0C10.863,0 14,3.137 14,7L13.863,8.353L10.01,4.5L6.51,8L4.01,5.5L0.369,9.141Z\' style=\'fill:rgb(128,128,255);fill-rule:nonzero;\'/></g><g transform=\'matrix(1.42857,0,0,1.42857,22.5,10.5)\'><path d=\'M0.839,10.151L0.369,9.141L4.01,5.5L6.51,8L10.01,4.5L13.863,8.353C13.787,8.748 13.662,9.131 13.522,9.5L3.151,12.845C2.858,12.651 2.572,12.429 2.313,12.194L0.839,10.151Z\' style=\'fill:white;fill-rule:nonzero;\'/></g><g transform=\'matrix(1.42857,0,0,1.42857,22.5,10.5)\'><path d=\'M13.522,9.5C12.532,12.14 9.983,14 7,14C5.574,14 4.247,13.579 3.151,12.845L10.01,5.979L13.522,9.5Z\' style=\'fill:rgb(51,51,204);fill-rule:nonzero;\'/></g><g transform=\'matrix(1.42857,0,0,1.42857,22.5,10.5)\'><path d=\'M0.839,10.151L4.01,6.979L5.771,8.74L2.312,12.194C1.721,11.562 1.233,10.881 0.839,10.151Z\' style=\'fill:rgb(51,51,204);fill-rule:nonzero;\'/></g><g transform=\'matrix(1.42857,0,0,1.42857,22.5,10.5)\'><circle cx=\'6\' cy=\'3.5\' r=\'1.5\' style=\'fill:white;\'/></g></g></svg>'
  335. };
  336. function get_SVG_UI_Icon(icon_name) { return `url("data:image/svg+xml;utf8,${ SVG_UI_Icons[icon_name] }")`; }
  337. const SVG_UI_File_Icons = { // n.b.: order is important
  338. 'file_icon_dir': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m6 2.5-1-1.5h-5v12h14v-10.5z\' fill=\'%2339f\'/><path d=\'m1.5 4h11v7.5h-11z\' fill=\'%239cf\'/></svg>',
  339. 'file_icon_dir_open': '<svg viewBox=\'0 0 14 14\' clip-rule=\'evenodd\' fill-rule=\'evenodd\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m6.1 2.7-1.3-1.7h-4.8v12h14v-10.3z\' fill=\'%2339f\' fill-rule=\'nonzero\'/><path d=\'m7 6h5.5v5.5h-11z\' fill=\'%239cf\'/></svg>',
  340. 'file_icon_file': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m8.3 0h-6.8v14h11v-9.8l-4.2-4.2z\' fill=\'%23888\'/><g fill=\'%23fff\'><path d=\'m11 12.5h-8v-11h3.8v4.2h4.2z\'/><path d=\'m8.3 4.2h1.9l-1.9-2z\'/></g></svg>',
  341. 'file_icon_invisible': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m8.3 0h-6.8v14h11v-9.8l-4.2-4.2z\' fill=\'%23888\'/><path d=\'m11 12.5h-8v-11h3.8v4.2h4.2z\' fill=\'%23bbb\'/><path d=\'m8.3 4.2h1.9l-1.9-2z\' fill=\'%23bbb\'/><circle cx=\'7\' cy=\'9\' fill=\'%23878787\' r=\'1.5\'/></svg>',
  342. 'file_icon_ignored': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'M 10.695,1.774 1.856,10.613 3.482,12.239 12.321,3.4 Z M 7,2 c 2.8,0 5,2.2 5,5 0,2.8 -2.2,5 -5,5 C 4.2,12 2,9.8 2,7 2,4.2 4.2,2 7,2 M 7,0 C 3.1,0 0,3.1 0,7 c 0,3.9 3.1,7 7,7 3.9,0 7,-3.1 7,-7 C 14,3.1 10.9,0 7,0 Z\' style=\'fill:%23888888;fill-opacity:1\' /></svg>',
  343. 'file_icon_dirinvisible': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m6 2.5-1-1.5h-5v12h14v-10.5z\' fill=\'%23888\'/><path d=\'m1.5 4h11v7.5h-11z\' fill=\'%23bbb\'/><circle cx=\'7\' cy=\'7.5\' fill=\'%23888\' r=\'1.5\'/></svg>',
  344. 'file_icon_alias': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0h14v14h-14z\' fill=\'%23808080\'/><path d=\'m3 12.5c0-3.863 2.253-7.5 6.259-7.5\' fill=\'none\' stroke=\'%23fc6\' stroke-width=\'3\'/><path d=\'m13 5-4-4v8z\' fill=\'%23fc6\'/></svg>',
  345. 'file_icon_archive': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m11 8.5v-1h2v2l-5 1h-2v1.5h4v1h-4v1h-3v-1h-2v-1h2v-1.5h-2v-2h2v-6.5h-2v-2h7l5 1v2h-2v-1h-5v6.5z\' fill=\'%23666\'/></svg>',
  346. 'file_icon_app': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m6.125 0-.292 1.859c-.587.135-1.146.38-1.64.693v-.018l-1.532-1.094-1.221 1.221 1.094 1.532h.018c-.313.495-.559 1.051-.693 1.64l-1.859.292v1.75l1.859.292c.134.589.38 1.145.693 1.64h-.018l-1.094 1.532 1.221 1.221 1.532-1.094v-.018c.494.313 1.053.558 1.64.693l.292 1.859h1.75l.292-1.859c.596-.137 1.14-.372 1.64-.693l1.532 1.112 1.221-1.221-1.112-1.532c.309-.492.523-1.057.656-1.64l1.896-.292v-1.75l-1.896-.292c-.133-.583-.347-1.148-.656-1.64h.018l1.094-1.532-1.221-1.221-1.532 1.094v.018c-.5-.321-1.044-.556-1.64-.693l-.292-1.859h-1.75zm.875 4.667c1.288 0 2.333 1.036 2.333 2.333s-1.045 2.333-2.333 2.333-2.333-1.036-2.333-2.333 1.045-2.333 2.333-2.333z\' fill=\'%237a7ab8\'/></svg>',
  347. 'file_icon_audio': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><circle cx=\'7\' cy=\'7\' fill=\'%230f8a8a\' r=\'7\'/><g fill=\'%23fff\'><path d=\'m11 9.5c-.019.681-.796 1.339-1.75 1.475-.966.138-1.75-.31-1.75-1s.784-1.362 1.75-1.5c.268-.038.523-.031.75.013v-4.488h-4v6.5c-.019.681-.796 1.339-1.75 1.475-.966.138-1.75-.31-1.75-1s.784-1.362 1.75-1.5c.268-.038.523-.031.75.013v-6.488l6-1z\'/><path d=\'m11 2-6 1v2l6-1z\'/></g></svg>',
  348. 'file_icon_code': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m14 0h-14v14h14z\' fill=\'%2372d\'/><g fill=\'%23fff\'><path d=\'m5.923 12.965c-1.049 0-1.784-.161-2.209-.48-.425-.317-.638-.82-.638-1.503v-2.067c0-.446-.146-.764-.438-.95-.292-.188-.709-.281-1.256-.281v-1.368c.547 0 .967-.094 1.259-.28s.438-.5.438-.938v-2.092c0-.675.217-1.172.65-1.491.432-.32 1.164-.479 2.195-.479v1.312c-.401.01-.718.09-.952.24-.233.15-.348.426-.348.827v1.985c0 .876-.511 1.396-1.532 1.559v.083c1.021.154 1.532.67 1.532 1.544v1.997c0 .41.116.688.349.835.233.146.55.223.951.232z\'/><path d=\'m8.076 12.965v-1.313c.392-.009.706-.089.944-.239.236-.15.355-.426.355-.829v-1.996c0-.867.511-1.382 1.531-1.545v-.084c-1.02-.164-1.53-.679-1.53-1.546v-1.997c0-.41-.116-.688-.349-.834-.232-.146-.549-.224-.951-.233v-1.313c1.049 0 1.785.159 2.21.479.423.319.637.821.637 1.505v2.065c0 .447.146.765.438.951.292.187.711.28 1.257.28v1.367c-.546.012-.967.107-1.259.287-.293.183-.438.5-.438.945v2.08c0 .674-.217 1.172-.65 1.491-.432.319-1.165.479-2.195.479z\'/></g></svg>',
  349. 'file_icon_database': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m14 2.5v9c0 1.38-3.137 2.5-7 2.5s-7-1.12-7-2.5v-9\' fill=\'%23808080\'/><path d=\'m13 2.5v9c0 .828-2.689 1.5-6 1.5s-6-.672-6-1.5v-9\' fill=\'%23b4b4b4\'/><path d=\'m14 8.5c0 1.38-3.137 2.5-7 2.5s-7-1.12-7-2.5\' fill=\'%23808080\'/><path d=\'m13 8.5c0 .828-2.689 1.5-6 1.5s-6-.672-6-1.5\' fill=\'%23b4b4b4\'/><path d=\'m14 5.5c0 1.38-3.137 2.5-7 2.5s-7-1.12-7-2.5\' fill=\'%23808080\'/><path d=\'m13 5.5c0 .828-2.689 1.5-6 1.5s-6-.672-6-1.5\' fill=\'%23b4b4b4\'/><ellipse cx=\'7\' cy=\'2.5\' fill=\'%23808080\' rx=\'7\' ry=\'2.5\'/><ellipse cx=\'7\' cy=\'2.5\' fill=\'%23b4b4b4\' rx=\'5.5\' ry=\'1.5\'/></svg>',
  350. 'file_icon_ebook': '<svg clip-rule=\'evenodd\' fill-rule=\'evenodd\' stroke-linejoin=\'round\' stroke-miterlimit=\'2\' viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m2.668-.001c1.705.001 3.492.35 4.332 1.257.84-.908 2.627-1.256 4.332-1.257h2.668v12.541c-.818 0-2.181.005-3 .023-1.184.026-3.008.42-3 1.437l-1-.017-1 .017c.008-1.017-2-1.437-3-1.437-.819 0-2.182-.023-3-.023v-12.541zm-1.168 1.5v9.501h1.286c1.086.025 2.213.081 3.204.568l.01.006v-8.576c0-1.136-1.49-1.398-2.336-1.47-.708-.059-1.438-.029-2.164-.029zm11 0c-.726 0-1.456-.03-2.164.029-.846.072-2.336.334-2.336 1.47v8.576l.01-.006c.991-.487 2.118-.543 3.204-.568h1.286z\' fill=\'%23222\' fill-rule=\'nonzero\'/></svg>',
  351. 'file_icon_font': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m14 0h-14v14h14z\' fill=\'%23709\'/><path d=\'m4.678 11.179h1.393v-8.266h-2.616v1.052h-1.455v-2.553h10v2.554h-1.456v-1.053h-2.599v8.266h1.347v1.409h-4.614z\' fill=\'%23fff\'/></svg>',
  352. 'file_icon_graphics': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0h14v14h-14z\' fill=\'%23808080\'/><path d=\'m7.774 8.285 4.726 4.715-8-3.525-1.5-4.975h-2v-3.5h3.525l-.025 2 5 1.5 3.5 8-4.7-4.752c.127-.22.2-.476.2-.748 0-.828-.672-1.5-1.5-1.5s-1.5.672-1.5 1.5.672 1.5 1.5 1.5c.283 0 .548-.079.774-.215z\' fill=\'%23ccc\'/></svg>',
  353. 'file_icon_htm': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m6.967.5c-3.553.018-6.467 2.947-6.467 6.5 0 3.566 2.934 6.5 6.5 6.5s6.5-2.934 6.5-6.5c0-3.553-2.914-6.482-6.467-6.5zm.033 0v13m6.5-6.5h-13m1.467-4c3.004 2.143 7.062 2.143 10.066 0m0 8c-3.004-2.143-7.062-2.143-10.066 0m4.533-10.333c-1.874 1.582-2.957 3.914-2.957 6.366 0 2.453 1.083 4.785 2.957 6.367m1 0c1.874-1.582 2.957-3.914 2.957-6.367 0-2.452-1.083-4.784-2.957-6.366\' fill=\'%23fff\' stroke=\'%23e44d26\'/></svg>',
  354. 'file_icon_ignoredimage': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m.369 9.141c-.252-.678-.369-1.396-.369-2.141 0-3.863 3.137-7 7-7s7 3.137 7 7l-.137 1.353-3.853-3.853-3.5 3.5-2.5-2.5z\' fill=\'%23808080\'/><path d=\'m.839 10.151-.47-1.01 3.641-3.641 2.5 2.5 3.5-3.5 3.853 3.853c-.076.395-.201.778-.341 1.147l-10.371 3.345c-.293-.194-.579-.416-.838-.651z\' fill=\'%23fff\'/><path d=\'m13.522 9.5c-.99 2.64-3.539 4.5-6.522 4.5-1.426 0-2.753-.421-3.849-1.155l6.859-6.866z\' fill=\'%23808080\'/><path d=\'m.839 10.151 3.171-3.172 1.761 1.761-3.459 3.454c-.591-.632-1.079-1.313-1.473-2.043z\' fill=\'%23808080\'/><circle cx=\'6\' cy=\'3.5\' fill=\'%23fff\' r=\'1.5\'/></svg>',
  355. 'file_icon_image': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m.369 9.141c-.252-.678-.369-1.396-.369-2.141 0-3.863 3.137-7 7-7s7 3.137 7 7l-.137 1.353-3.853-3.853-3.5 3.5-2.5-2.5z\' fill=\'%238080ff\'/><path d=\'m.839 10.151-.47-1.01 3.641-3.641 2.5 2.5 3.5-3.5 3.853 3.853c-.076.395-.201.778-.341 1.147l-10.371 3.345c-.293-.194-.579-.416-.838-.651z\' fill=\'%23fff\'/><path d=\'m13.522 9.5c-.99 2.64-3.539 4.5-6.522 4.5-1.426 0-2.753-.421-3.849-1.155l6.859-6.866z\' fill=\'%2333c\'/><path d=\'m.839 10.151 3.171-3.172 1.761 1.761-3.459 3.454c-.591-.632-1.079-1.313-1.473-2.043z\' fill=\'%2333c\'/><circle cx=\'6\' cy=\'3.5\' fill=\'%23fff\' r=\'1.5\'/></svg>',
  356. 'file_icon_markdown': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m14 0h-14v14h14z\' fill=\'%236a6a95\'/><path d=\'m12 11.5h-2.5v-5.143l-2.5 2.948-2.5-2.948v5.143h-2.5v-9h2.273l2.721 3.377 2.733-3.377h2.273z\' fill=\'%23ddd\'/></svg>',
  357. 'file_icon_office': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0h14v14h-14z\' fill=\'%23808080\'/><g fill=\'%23cdcdcd\'><path d=\'m10 1.5h2.5v1h-2.5z\'/><path d=\'m10 4h2.5v1h-2.5z\'/><path d=\'m10 6.5h2.5v1h-2.5z\'/><path d=\'m10 9h2.5v1h-2.5z\'/><path d=\'m10 11.5h2.5v1h-2.5z\'/><path d=\'m6.5 1.5h2.5v1h-2.5z\'/><path d=\'m6.5 4h2.5v1h-2.5z\'/><path d=\'m6.5 6.5h2.5v1h-2.5z\'/><path d=\'m6.5 9h2.5v1h-2.5z\'/><path d=\'m6.5 11.5h2.5v1h-2.5z\'/><path d=\'m1.5 1.5h4v11h-4z\'/></g></svg>',
  358. 'file_icon_pdf': '<svg clip-rule=\'evenodd\' fill-rule=\'evenodd\' viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0h13.999986v13.999986h-13.999986z\' fill=\'%23e0382d\' stroke-width=\'.259259\'/><path d=\'m45 31.2c-2.6-2.7-9.7-1.6-11.4-1.4-2.5-2.4-4.2-5.3-4.8-6.3.9-2.7 1.5-5.4 1.6-8.3 0-2.5-1-5.2-3.8-5.2-1 0-1.9.6-2.4 1.4-1.2 2.1-.7 6.3 1.2 10.6-1.1 3.1-2.1 6.1-4.9 11.4-2.9 1.2-9 4-9.5 7-.2.9.1 1.8.8 2.5.7.6 1.6.9 2.5.9 3.7 0 7.3-5.1 9.8-9.4 2.1-.7 5.4-1.7 8.7-2.3 3.9 3.4 7.3 3.9 9.1 3.9 2.4 0 3.3-1 3.6-1.9.5-1 .2-2.1-.5-2.9zm-2.5 1.7c-.1.7-1 1.4-2.6 1-1.9-.5-3.6-1.4-5.1-2.6 1.3-.2 4.2-.5 6.3-.1.8.2 1.6.7 1.4 1.7zm-16.7-20.6c.2-.3.5-.5.8-.5.9 0 1.1 1.1 1.1 2-.1 2.1-.5 4.2-1.2 6.2-1.5-4-1.2-6.8-.7-7.7zm-.2 19.4c.8-1.6 1.9-4.4 2.3-5.6.9 1.5 2.4 3.3 3.2 4.1 0 .1-3.1.7-5.5 1.5zm-5.9 4c-2.3 3.8-4.7 6.2-6 6.2-.2 0-.4-.1-.6-.2-.3-.2-.4-.5-.3-.9.3-1.4 2.9-3.3 6.9-5.1z\' fill=\'%23fff\' fill-rule=\'nonzero\' transform=\'matrix(.344737 0 0 .35503 -2.77114 -2.5503)\'/></svg>',
  359. 'file_icon_playlist': '<svg viewBox=\'0 0 14 14\' clip-rule=\'evenodd\' fill-rule=\'evenodd\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0h14v14h-14z\' fill=\'%23888\' fill-rule=\'nonzero\'/><path d=\'m1.5 1.5h8v1h-8zm0 2.5h8v1h-8zm0 2.5h8v1h-8zm0 2.5h7v1h-7zm0 2.5h5.5v1h-5.5zm9.5-10h1v10c-.019.681-.796 1.339-1.75 1.475-.966.138-1.75-.31-1.75-1s.784-1.362 1.75-1.5a2.28 2.28 0 0 1 .75.013z\' fill=\'%23fff\'/></svg>',
  360. 'file_icon_text': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m14 0h-14v14h14z\' fill=\'%236a6a95\'/><g fill=\'%23fff\'><path d=\'m6.5 1.5h6v1h-6z\'/><path d=\'m1.5 1.5h3.5v3.5h-3.5z\'/><path d=\'m1.5 6.5h11v1h-11z\'/><path d=\'m6.5 4h6v1h-6z\'/><path d=\'m1.5 11.5h8v1h-8z\'/><path d=\'m1.5 9h11v1h-11z\'/></g></svg>',
  361. 'file_icon_video': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m14 14v-14h-14v14z\'/><g fill=\'%23fff\'><path d=\'m9.5 3v-2h-2v2z\'/><path d=\'m3.5 3v-2h-2v2z\'/><path d=\'m6.5 3v-2h-2v2z\'/><path d=\'m12.5 3v-2h-2v2z\'/><path d=\'m9.5 13v-2h-2v2z\'/><path d=\'m3.5 13v-2h-2v2z\'/><path d=\'m6.5 13v-2h-2v2z\'/><path d=\'m12.5 13v-2h-2v2z\'/></g><path d=\'m12.5 10v-6h-11v6z\' fill=\'%23eda412\'/></svg>',
  362. 'file_icon_bin': '', 'file_icon_other': '', // <-- these two use file_icon_system:
  363. 'file_icon_system': '<svg viewBox=\'0 0 14 14\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m0 0h14v14h-14z\' fill=\'%23808080\'/><g fill=\'%23ccc\'><path d=\'m1.247 6.495h3.263v-1.067h-.881v-3.835h-.974c-.371.232-.727.371-1.284.479v.82h.928v2.536h-1.052z\'/><path d=\'m7 6.588c1.082 0 1.825-.89 1.825-2.567 0-1.67-.743-2.521-1.825-2.521s-1.825.843-1.825 2.521c0 1.677.743 2.567 1.825 2.567zm0-1.021c-.309 0-.572-.247-.572-1.546s.263-1.5.572-1.5.572.201.572 1.5-.263 1.546-.572 1.546z\'/><path d=\'m9.598 6.495h3.263v-1.067h-.882v-3.835h-.974c-.371.232-.727.371-1.283.479v.82h.927v2.536h-1.051z\'/><path d=\'m2.825 12.588c1.082 0 1.824-.89 1.824-2.567 0-1.67-.742-2.521-1.824-2.521-1.083 0-1.825.843-1.825 2.521 0 1.677.742 2.567 1.825 2.567zm0-1.021c-.31 0-.572-.247-.572-1.546s.262-1.5.572-1.5c.309 0 .572.201.572 1.5s-.263 1.546-.572 1.546z\'/><path d=\'m5.423 12.495h3.263v-1.067h-.882v-3.835h-.974c-.371.232-.727.371-1.284.479v.82h.928v2.536h-1.051z\'/><path d=\'m11.175 12.588c1.083 0 1.825-.89 1.825-2.567 0-1.67-.742-2.521-1.825-2.521-1.082 0-1.824.843-1.824 2.521 0 1.677.742 2.567 1.824 2.567zm0-1.021c-.309 0-.572-.247-.572-1.546s.263-1.5.572-1.5c.31 0 .572.201.572 1.5s-.262 1.546-.572 1.546z\'/></g></svg>'
  364. };
  365. const SVG_Text_Editing_UI_Icons = {
  366. 'toggle_theme': '<svg viewBox=\'0 0 16 16\' clip-rule=\'evenodd\' fill-rule=\'evenodd\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'m8 0c4.415 0 8 3.585 8 8s-3.585 8-8 8-8-3.585-8-8 3.585-8 8-8zm0 2c3.311 0 6 2.689 6 6s-2.689 6-6 6z\' fill=\'%23333\'/></svg>',
  367. 'text_editing': '<svg version=\'1.1\' id=\'Layer_1\' xmlns:serif=\'http://www.serif.com/\' xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\' x=\'0px\' y=\'0px\' viewBox=\'0 0 1297.6 1301.6\' style=\'enable-background:new 0 0 1297.6 1301.6;\' xml:space=\'preserve\'><polygon style=\'fill:%23222;\' points=\'1049,600.6 1049,1150.6 149,1150.6 149,250.6 699,250.6 699,101.6 0,101.6 0,1301.6 1200,1301.6 1200,600.6 \'/><rect x=\'421.7\' y=\'375.9\' transform=\'matrix(0.7071 -0.7071 0.7071 0.7071 -95.8103 720.4032)\' style=\'fill:%23222;\' width=\'800\' height=\'200\'/><rect x=\'1113.4\' y=\'17.7\' transform=\'matrix(0.7071 -0.7071 0.7071 0.7071 262.3587 868.8007)\' style=\'fill:%23222;\' width=\'133\' height=\'200\'/><polyline style=\'fill:%23222;\' points=\'345.9,951.8 439.9,716.3 581.3,857.7 \'/></svg>',
  368. 'show_markdown': '<svg viewBox=\'0 0 100 60\' xmlns=\'http://www.w3.org/2000/svg\' width=\'100\'><g fill=\'%23333\'><path d=\'M42.215 60l.17-46.24h-.255L30.06 60h-7.99L10.255 13.76H10L10.169 60H.905V-.18H14.59l11.56 44.03h.34L37.794-.18H52.16V60h-9.945zM99.589 29.996c0 9.519-1.997 16.901-5.992 22.142C89.602 57.38 83.722 60 75.959 60H60.914V-.18h15.13c7.706 0 13.558 2.65 17.553 7.948 3.995 5.299 5.992 12.708 5.992 22.228zm-10.2 0c0-3.57-.326-6.686-.978-9.35-.651-2.663-1.572-4.873-2.762-6.63-1.19-1.756-2.607-3.073-4.25-3.953-1.645-.878-3.43-1.317-5.355-1.317h-4.845v42.33h4.845c1.926 0 3.711-.438 5.355-1.317 1.643-.878 3.06-2.195 4.25-3.953 1.189-1.756 2.11-3.952 2.762-6.587.651-2.637.978-5.709.978-9.223z\'/></g></svg>',
  369. 'show_source': '<svg viewBox=\'0 0 22 14\' xmlns=\'http://www.w3.org/2000/svg\' fill-rule=\'evenodd\' clip-rule=\'evenodd\'><path fill=\'none\' d=\'M0 0h21.996v14H0z\'/><clipPath id=\'a\'><path d=\'M0 0h21.996v14H0z\'/></clipPath><g clip-path=\'url(%23a)\' fill=\'%23333\'><path d=\'M0 7.393v-.786l6.062-3.5.75 1.3L2.32 7l4.492 2.593-.75 1.3L0 7.393zM21.996 6.607v.786l-6.062 3.5-.75-1.3L19.676 7l-4.492-2.593.75-1.3 6.062 3.5zM15.15 1.313l-1.3-.75-7 12.124 1.3.75 7-12.124z\'/></g></svg>',
  370. 'show_preview': '<svg viewBox=\'0 0 16 16\' xmlns=\'http://www.w3.org/2000/svg\' fill-rule=\'evenodd\' clip-rule=\'evenodd\'><path d=\'M10 2.5V1H0v1.5h4V15h2V2.5h4zM9 6.5V8h2v4.053c0 2.211 1.547 3.442 3 3.442.989 0 1.556-.258 2-.495v-1.5c-.565.257-.882.376-1.507.376-.847 0-1.493-.474-1.493-1.876V8h2.5V6.5H13v-3h-1.98v3H9z\' fill=\'%23333\' fill-rule=\'nonzero\'/></svg>',
  371. 'show_html': '<svg viewBox=\'0 0 22 16\' xmlns=\'http://www.w3.org/2000/svg\' fill-rule=\'evenodd\' clip-rule=\'evenodd\'><path fill=\'none\' d=\'M0 0h21.022v16H0z\'/><clipPath id=\'a\'><path d=\'M0 0h21.022v16H0z\'/></clipPath><g clip-path=\'url(%23a)\' fill=\'%23333\'><path d=\'M7.732.222L9.5 1.99 3.49 8l6.01 6.01-1.768 1.768L-.046 8 7.732.222zM13.268 15.778L11.5 14.01 17.51 8 11.5 1.99 13.268.222 21.046 8l-7.778 7.778z\'/></g></svg>',
  372. 'toggle_split': '<svg viewBox=\'0 0 16 16\' xmlns=\'http://www.w3.org/2000/svg\' fill-rule=\'evenodd\' clip-rule=\'evenodd\'><path d=\'M0 0v16h16V0H0zm14 14H9V2h5v12zm-7 0H2V2h5v12z\' fill=\'%23333\' fill-rule=\'nonzero\'/></svg>',
  373. 'save_btn': '<svg viewBox=\'0 0 16 16\' xmlns=\'http://www.w3.org/2000/svg\' fill-rule=\'evenodd\' clip-rule=\'evenodd\'><g fill=\'currentColor\'><path d=\'M16 0v10.02L14 10V2H2v8l-2 .02V0h16z\' fill-rule=\'nonzero\'/><path d=\'M7 5h2v9H7z\'/><path d=\'M3.757 11.757l1.415-1.414L8 13.172l2.828-2.829 1.415 1.414L8 16l-4.243-4.243z\'/></g></svg>',
  374. 'save_btn_edited': '<svg viewBox=\'0 0 16 16\' xmlns=\'http://www.w3.org/2000/svg\' fill-rule=\'evenodd\' clip-rule=\'evenodd\'><g fill=\'%23DD2222\'><path d=\'M16 0v10.02L14 10V2H2v8l-2 .02V0h16z\' fill-rule=\'nonzero\'/><path d=\'M7 5h2v9H7z\'/><path d=\'M3.757 11.757l1.415-1.414L8 13.172l2.828-2.829 1.415 1.414L8 16l-4.243-4.243z\'/></g></svg>'
  375. };
  376. // ===> GET SVG UI ICON by name
  377. function get_SVG_UI_File_Icon(icon_name) { // ===> GET SVG UI FILE Icon by name
  378. switch(icon_name) {
  379. case 'favicon': return '<link href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAgMAAAC+UIlYAAAACVBMVEUmRcmZzP8zmf8pVcWPAAAAAXRSTlMAQObYZgAAAFBJREFUeF7tyqERwDAMBEE3mX5UiqDmqwwziTPHjG7xrmzrLFtRaApDIRiKQlMYCsFQFJrCUAiGotAU5hTA1WB4fhkMBsOJwWAwgHvB8CHpBcTbpxy4RZNvAAAAAElFTkSuQmCC" rel="icon" sizes="16x16" />';
  380. case 'file_icon_dir_default': return 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAd5JREFUeNqMU79rFUEQ/vbuodFEEkzAImBpkUabFP4ldpaJhZXYm/RiZWsv/hkWFglBUyTIgyAIIfgIRjHv3r39MePM7N3LcbxAFvZ2b2bn22/mm3XMjF+HL3YW7q28YSIw8mBKoBihhhgCsoORot9d3/ywg3YowMXwNde/PzGnk2vn6PitrT+/PGeNaecg4+qNY3D43vy16A5wDDd4Aqg/ngmrjl/GoN0U5V1QquHQG3q+TPDVhVwyBffcmQGJmSVfyZk7R3SngI4JKfwDJ2+05zIg8gbiereTZRHhJ5KCMOwDFLjhoBTn2g0ghagfKeIYJDPFyibJVBtTREwq60SpYvh5++PpwatHsxSm9QRLSQpEVSd7/TYJUb49TX7gztpjjEffnoVw66+Ytovs14Yp7HaKmUXeX9rKUoMoLNW3srqI5fWn8JejrVkK0QcrkFLOgS39yoKUQe292WJ1guUHG8K2o8K00oO1BTvXoW4yasclUTgZYJY9aFNfAThX5CZRmczAV52oAPoupHhWRIUUAOoyUIlYVaAa/VbLbyiZUiyFbjQFNwiZQSGl4IDy9sO5Wrty0QLKhdZPxmgGcDo8ejn+c/6eiK9poz15Kw7Dr/vN/z6W7q++091/AQYA5mZ8GYJ9K0AAAAAASUVORK5CYII= ")';
  381. case 'file_icon_file_default': return 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABHUlEQVR42o2RMW7DIBiF3498iHRJD5JKHurL+CRVBp+i2T16tTynF2gO0KSb5ZrBBl4HHDBuK/WXACH4eO9/CAAAbdvijzLGNE1TVZXfZuHg6XCAQESAZXbOKaXO57eiKG6ft9PrKQIkCQqFoIiQFBGlFIB5nvM8t9aOX2Nd18oDzjnPgCDpn/BH4zh2XZdlWVmWiUK4IgCBoFMUz9eP6zRN75cLgEQhcmTQIbl72O0f9865qLAAsURAAgKBJKEtgLXWvyjLuFsThCSstb8rBCaAQhDYWgIZ7myM+TUBjDHrHlZcbMYYk34cN0YSLcgS+wL0fe9TXDMbY33fR2AYBvyQ8L0Gk8MwREBrTfKe4TpTzwhArXWi8HI84h/1DfwI5mhxJamFAAAAAElFTkSuQmCC ")';
  382. default: return 'url("data:image/svg+xml;utf8,'+ SVG_UI_File_Icons[icon_name] +'")';
  383. }
  384. }
  385. function CSS_UI_Icon_Rules() { // programatically add File icon CSS rules // ===> CSS UI ICON RULES
  386. let rules = '', kind, class_name;
  387. for ( let icon in SVG_UI_File_Icons ) {
  388. kind = icon.slice(icon.lastIndexOf('_') + 1);
  389. class_name = kind;
  390. if ( class_name !== ('file') ) { // exceptions:
  391. if ( kind === 'dirinvisible' ) { class_name = 'dir.invisible'; }
  392. if ( kind === 'ignoredimage' ) { class_name = 'ignored_image'; }
  393. if ( kind === 'open' ) { class_name = 'has_subdirectory'; kind = 'dir_open'; }
  394. if ( /alias|symlink/.test(kind) ) { class_name = 'link'; }
  395. if ( /bin|other/.test(kind) ) { kind = 'system'; }
  396. // add rules for dir_list items, content_header, stats details:
  397. rules += `body:not(.use_custom_icons_false) .${ class_name } .has_icon_before_before, #content_pane[data-content^="has_${ class_name }"] #content_title span::before,body:not(.use_custom_icons_false) .${ class_name }.has_icon_before::before, .${ class_name } .has_icon_before::before { background-image: url("data:image/svg+xml;utf8,${ SVG_UI_File_Icons['file_icon_'+kind] }"); }`; // add custom file icons
  398. }
  399. }
  400. return rules;
  401. }
  402. // END SVG UI ICONS
  403. //==============================// UI HTML
  404. // SIDEBAR ELEMENTS
  405. function Sidebar_Elements(body_id,parent_link) { // Assemble directory elements for both top and iframe directories
  406. const sidebar_header_menu_elements = `
  407. <li id="go_to_item" class="no_checkmark border_bottom" title="Go to item..."><span class="menu_item">Go to item&hellip; (&#8984;&#8679;J)</span><input id="go_to_item_input" class="display_none resize_none whitespace_pre" rows="1" placeholder="Item row number" spellcheck="false" /></li>
  408. <li id="menu_sort_by" class="has_submenu border_bottom error_display_none"><span class="menu_item">Sort by&hellip;</span>${SVG_UI_Icons.arrow}
  409. <ul id="sort_menu" class="submenu box_shadow_menu background_grey_80 border_all"> <li id="menu_sort_by_name" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_name"><span class="menu_item">Name</span></li> <li id="menu_sort_by_duration" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_duration"><span class="menu_item">Duration</span></li> <li id="menu_sort_by_size" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_size"><span class="menu_item">Size</span></li> <li id="menu_sort_by_date" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_date"><span class="menu_item">Date</span></li> <li id="menu_sort_by_kind" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_kind"><span class="menu_item">Kind</span></li> <li id="menu_sort_by_ext" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_ext"><span class="menu_item">Extension</span></li> <li id="menu_sort_by_default" class="is_submenu_item toggle_UI_pref sorting" data-ui_pref="sort_by_default"><span class="menu_item">Default</span></li> </ul></li>
  410. <li id="UI_settings" class="has_submenu"><span class="menu_item">UI Preferences</span>${SVG_UI_Icons.arrow}
  411. <ul id="UI_settings_submenu" class="submenu box_shadow_menu background_grey_80 border_all"> <li id="menu_theme_container" class="is_submenu_item" title="Set the main UI theme (light or dark)."><span id="menu_theme" class="toggle_UI_pref menu_item checkmark ignore_warning" data-ui_pref="theme"><span> Theme</span></span></li> <li id="alternate_background" class="is_submenu_item toggle_UI_pref ignore_warning" data-ui_pref="alternate_background" title="Alternate backgrounds of directory items."><span class="menu_item checkmark">Alternate Backgrounds</span></li> <li id="show_numbers" class="is_submenu_item toggle_UI_pref ignore_warning border_bottom error_display_none" data-ui_pref="show_numbers" title="Number directory list items."><span class="menu_item checkmark">Show Numbers</span></li> <li id="use_custom_icons" class="is_submenu_item toggle_UI_pref ignore_warning" data-ui_pref="use_custom_icons" title="Use custom or browser/server default file and dir icons"><span class="menu_item checkmark">Use Custom Icons</span></li> <li id="show_image_thumbnails" class="is_submenu_item toggle_UI_pref ignore_warning" data-ui_pref="show_image_thumbnails" title="Show image thumbnails in directory list items."><span class="menu_item checkmark">Show Image Thumbnails</span></li> <li id="show_large_image_thumbnails" class="is_submenu_item toggle_UI_pref ignore_warning" data-ui_pref="show_large_image_thumbnails" title="Use large image thumbnails."><span class="menu_item checkmark">Use Large Image Thumbnails</span></li> <li id="show_image_thumbnails_always" class="is_submenu_item toggle_UI_pref ignore_warning border_bottom" data-ui_pref="show_image_thumbnails_always" title="Always show image thumbnails no matter how many images are in directory."><span class="menu_item checkmark">Always Show Image Thumbnails</span></li> <li id="ui_font" class="is_submenu_item ignore_warning no_checkmark" data-ui_pref="ui_font" title="Enter the name of an installed font."><span class="menu_item">Set UI Font&hellip;</span><input id="ui_font_input" class="display_none resize_none whitespace_pre" rows="1" placeholder="CSS Font Family" spellcheck="false" /></li> <li id="ui_scale" class="is_submenu_item ignore_warning display_flex flex_column no_checkmark" data-ui_pref="ui_scale" title="Scale the UI. Double-click to reset."><span class="menu_item">Scale UI&hellip;</span><span id="ui_scale_input_container" class="flex_justify_center_row padding_4_6"><input id="ui_scale_input" class="width_100 whitespace_pre display_block resize_none" type="range" min="75" max="125" step="1" placeholder="Scale UI"></span></li> </ul></li>
  412. <li id="file_handling" class="has_submenu error_display_none"><span class="menu_item">File Handling Preferences</span>${SVG_UI_Icons.arrow}
  413. <ul id="file_handling_submenu" class="submenu box_shadow_menu background_grey_80 border_all"> <li id="show_invisible_items" class="is_submenu_item toggle_UI_pref" data-ui_pref="show_invisibles" title="Show/hide invisible items"><span class="menu_item checkmark">Show Invisible Items (&#8984;&#8679;I)</span></li> <li id="show_ignored_items" class="is_submenu_item toggle_UI_pref" data-ui_pref="show_ignored_items" title="Show/hide ignored items (from the list of ignored file types in the user settings)."><span class="menu_item checkmark">Show Ignored Items</span></li> <li id="ignore_ignored_items" class="is_submenu_item toggle_UI_pref border_bottom" data-ui_pref="ignore_ignored_items" title="If checked, the browser will not attempt to load ignored items (from the list of ignored file types in the user settings). It is recommended to leave this checked."><span class="menu_item checkmark">Ignore Ignored Items</span></li> <li id="autoload_index_files" class="is_submenu_item toggle_UI_pref" data-ui_pref="autoload_index_files" title="Automatically load html index file."><span id="autoload_index_files_menu" class="menu_item checkmark">Autoload Index Files</span></li> </ul></li>
  414. <li id="media_settings" class="has_submenu error_display_none"><span class="menu_item">Media Preferences</span>${SVG_UI_Icons.arrow}
  415. <ul id="UI_settings_submenu" class="submenu box_shadow_menu background_grey_80 border_all"> <li id="media_autoload" class="is_submenu_item toggle_UI_pref" data-ui_pref="media_autoload" title="Automatically select and load the first media item in a directory and cover art (if any)."><span id="media_autoload_menu" class="menu_item checkmark">Autoload Media</span></li> <li id="media_autoplay" class="is_submenu_item toggle_UI_pref" data-ui_pref="media_autoplay" title="Automatically play the next media item."><span id="media_autoload_menu" class="menu_item checkmark">Autoplay Media</span></li> <li id="media_play_all" class="is_submenu_item toggle_UI_pref border_bottom" data-ui_pref="media_play_all" title="If checked, autoplay all media types (i.e., audio and video), else just autoplay the currently selected/playing media type."><span class="menu_item checkmark">Play All Media Files</span></li> <li id="loop_media_files" class="is_submenu_item" title="If checked, loop media playback."><span id="loop_media_menu" class="menu_item">Loop Media Playback</span></li> <li id="shuffle_media_files" class="is_submenu_item border_bottom" title="If checked, shuffle media playback."><span id="shuffle_media_menu" class="menu_item">Shuffle Media Playback</span></li> <li id="refresh_media_durations" class="is_submenu_item border_bottom" title=""><span id="refresh_media_durations_menu" class="menu_item">Refresh Media Durations</span></li> </ul></li>
  416. <li id="text_editing" class="has_submenu error_display_none"><span class="menu_item">Text Editing Preferences</span>${SVG_UI_Icons.arrow} <ul id="text_editing_menu" class="submenu box_shadow_menu background_grey_80 border_all"> <li id="texteditor_menu_item" class="is_submenu_item border_bottom" title="Toggle the main text editor."><span id="texteditor" class="menu_item">Toggle Main Text Editor</span></li> <li id="toggle_text_editing" class="error_display_none border_bottom" title="Enable/disable editing of text files. Does not effect main text editor."><span id="text_editing_enable" class="menu_item checkmark" data-ui_pref="text_editing_enable"><span id="disable">Text File Editing </span></span></li> <li id="texteditor_split_view" class="is_submenu_item toggle_UI_pref border_bottom" data-ui_pref="texteditor_split_view" title="Toggle display of default text view and both source and rendered text."><span class="menu_item checkmark">Split View</span></li> <li id="toggle_texteditor_raw_menu" class="is_submenu_item toggle_UI_pref" data-ui_pref="texteditor_view_raw"><span id="toggle_texteditor_raw" class="menu_item">View Source Text</span></li> <li id="toggle_texteditor_preview_menu" class="is_submenu_item toggle_UI_pref" data-ui_pref="texteditor_view_styled"><span id="toggle_texteditor_preview" class="menu_item">View Styled Text</span></li> <li id="toggle_texteditor_html_menu" class="is_submenu_item toggle_UI_pref" data-ui_pref="texteditor_view_html"><span id="toggle_texteditor_html" class="menu_item">View Rendered HTML</span></li></ul>
  417. <li id="default_settings" class="menu_item border_bottom"><span class="" href="#" title="Delete custom UI preferences stored in the URL query string and reload page.">Default Preferences</span></li>
  418. <li id="playlist_options" class="has_submenu border_bottom error_display_none"><span class="menu_item">Playlists</span>${SVG_UI_Icons.arrow} <ul id="playlist_menu" class="submenu box_shadow_menu background_grey_80 border_all"> <li id="open_playlist_container" class="is_submenu_item"><label id="open_playlist_label" class="menu_item" for="open_playlist" title="Open local .m3u playlist/filelist file.">Open Playlist/Filelist File&hellip;</label><input type="file" id="open_playlist" name="open_playlist" accept=".m3u,.m3u8"></li> <li id="close_playlist_container" class="is_submenu_item display_none"><span id="close_playlist" class="menu_item" href="#">Close Playlist/Filelist</span></li> <li class="is_submenu_item"><span id="make_playlist" class="menu_item border_top error_display_none" href="#" title="Make an .m3u playlist/filelist of the items in the current directory (if any).">Make Playlist/Filelist&hellip;</span></li> </ul></li>
  419. <li id="open_font_file" class="border_bottom error_display_none"><label id="open_font_label" class="menu_item" for="open_font" title="Open font file (.oft, .ttf, .woff) to view glyph repertoire and font info; save individual glyphs as .svg.">Open Font File&hellip;</label><input type="file" id="open_font" name="open_font" accept=".otf,.ttf,.woff"></li>
  420. <li id="about" class="menu_item" title="Go to script home page."><a id="about_link" class="flex_grow_1 ignore_warning" href="https://openuserjs.org/scripts/gaspar_schot/Supercharged_Local_Directory_File_Browser" target="_blank"><span class="icon_container"></span>Script Home &#8599;</a></li>
  421. <li id="show_help" class="menu_item" title="Show help."><span class="flex_grow_1"><span class="icon_container"></span>Help</span></li>
  422. <li id="donate" class="menu_item" title="Buy me a coffee!"><a id="donate_link" class="flex_grow_1 ignore_warning" href="https://www.buymeacoffee.com/fiLtliTFxQ" target="_blank" rel="noopener"><span class="icon_container"></span>Buy me a Coffee <svg xmlns="http://www.w3.org/2000/svg" width="14px" viewBox="0 0 64 64" enable-background="new 0 0 64 64"><path d="m32 2c-16.568 0-30 13.432-30 30 0 16.568 13.432 30 30 30s30-13.432 30-30c0-16.568-13.432-30-30-30m0 48c-1.371-1.814-20.53-12.883-16.602-25.218 3.53-11.073 15.094-6.597 16.602-.594 1.094-5.635 12.949-10.694 16.604.584 3.925 12.136-15.237 23.785-16.604 25.228" fill="#757575"/></svg></a></li>
  423. <li id="contact" class="menu_item" title="Send me an email."><a id="contact_link" class="flex_grow_1 ignore_warning" href="mailto:mshroud@protonmail.com"><span class="icon_container"></span>Contact</a></li>
  424. `;
  425. const sidebar_header_elements = function(body_id,parent_link) {
  426. let parent_links = createParentLinkItems(), sidebar_header_title_element = '', sidebar_header_menus ='', sidebar_texteditor_element ='';
  427. let checked = ( getCurrentUIPref('show_invisibles') === 'true' ? 'checked="true"' : '' );
  428. const sidebar_header_utilities_row_1 = `<div id="sidebar_header_utilities_row_1" class="background_grey_80"> <ul class="display_flex flex_row position_relative background_grey_80 border_bottom"> <li id="directory_buttons_left" class="display_flex no_highlight"> <button id="show_details" class="toggle_UI_pref pointer outline_none" data-ui_pref="show_details" tabindex="-1" title="Toggle display of directory item detail information (&#8984;&#8679;D)"><span id="show"> details</span></button></li> <li class="display_flex no_highlight"><label id="show_invisibles_container" for="inv_checkbox" class="margin_0 padding_0 flex_justify_center_row"><input class="toggle_UI_pref margin_0" type="checkbox" id="show_invisibles" data-ui_pref="show_invisibles" title="Toggle display of invisible items (&#8984&#8679;;I)" name="inv_checkbox" tabindex="-1"${ checked } /><span>&nbsp;Show Invisibles</span></label> </li> <li id="show_grid_btn" class="has_flyout_menu width_24px display_none position_relative pointer margin_0 padding_0 z_index_9997 no_highlight" tabindex="-1" title="Show Grid (&#8984;G)"><div class="display_flex width_14px_contents background_grey_80">${ SVG_UI_Icons.grid}</div> <ul class="menu has_popout_menu display_none position_absolute margin_0 padding_0 box_shadow_menu border_all background_grey_80"><div class="display_flex width_24px width_14px_contents">${ SVG_UI_Icons.grid}</div> <li id="show_image_grid" class="item_1 border_right border_bottom background_grey_80">Show Image Grid</li> <li id="show_font_grid" class="item_2 border_right background_grey_80">Show Font Grid</li> </ul> </li></ul></div>`;
  429. const sidebar_header_utilities_row_2 = `<div id="sidebar_header_utilities_row_2" class="background_grey_80"> <ul id="sorting_row_1" class="whitespace_pre pointer container display_flex flex_justify_contents border_bottom"> <li id="sort_by_name" class="whitespace_pre pointer toggle_UI_pref name sorting align_left no_highlight" data-ui_pref="sort_by_name" title="Sort by name"><span><input id="play_toggle" class="whitespace_pre pointer display_none position_relative" type="checkbox" tabindex="-1" checked="true" />Name</span></li> <li id="sort_by_default" class="whitespace_pre pointer toggle_UI_pref sorting align_right no_highlight" data-ui_pref="sort_by_default" title="Default sort"><span>Default</span></li> <li id="sort_by_duration" class="whitespace_pre pointer toggle_UI_pref sorting align_right display_none no_highlight" data-ui_pref="sort_by_duration" title="Sort by media duration"><span>Duration</span></li> </ul>
  430. <ul id="sorting_row_2" class="whitespace_pre pointer iframe_item border_bottom display_none"> <li id="sort_by_ext" class="whitespace_pre pointer toggle_UI_pref details sorting align_left no_highlight" data-ui_pref="sort_by_ext" title="Sort by extension"><span>Ext</span></li> <li id="sort_by_duration" class="whitespace_pre pointer toggle_UI_pref sorting align_right display_none no_highlight" data-ui_pref="sort_by_duration" title="Sort by media duration"><span>Duration</span></li> <li id="sort_by_size" class="whitespace_pre pointer toggle_UI_pref details sorting align_center no_highlight" data-ui_pref="sort_by_size" title="Sort by size"><span>Size</span></li> <li id="sort_by_date" class="whitespace_pre pointer toggle_UI_pref details sorting align_center no_highlight" data-ui_pref="sort_by_date" title="Sort by date"><span>Date</span></li> <li id="sort_by_kind" class="whitespace_pre pointer toggle_UI_pref details sorting align_right no_highlight" data-ui_pref="sort_by_kind" title="Sort by kind"><span>Kind</span></li> </ul> </div>`;
  431. switch(body_id) {
  432. case 'top_body':
  433. sidebar_header_title_element = `<div id="sidebar_header_title" class="display_flex flex_row border_bottom background_grey_75 normal"><div id="sidebar_header_title_div" class="align_center padding_4_6"></div></div>`;
  434. sidebar_header_menus = `<div id="sidebar_menus" class="display_flex flex_row background_grey_75 border_bottom pointer">
  435. <div id="sidebar_menu_parent" class="menu_container flex_justify_center width_24px padding_0"> <nav id="parent_dir_nav" class="flex_justify_center invert"><a href="${ parent_links[1] }" title="Parent Directory (&#8984;&uarr;)" class="flex_justify_center"><div class="display_flex">${ SVG_UI_Icons.chevron }${ SVG_UI_Icons.multiply }</div></a></nav> </div>
  436. <div id="sidebar_menu_parents" class="menu_container padding_0 flex_grow_1"> <nav id="parents_dir_nav" class="display_flex border_right line_height_1_4 border_left"> <div id="current_dir_path" class="bold flex_justify_center hyphens_none pointer z_index_9998" title="Parent Directories"><span class="has_icon_before">${ current_dir_path }</span></div> </nav> <ul id="parents_links" class="menu background_grey_85 position_absolute position_LR_0 border_top border_bottom margin_0 padding_0 display_none box_shadow_menu z_index_9998">${ parent_links[0] }</ul> </div>
  437. <div id="sidebar_menu_main_container" class="menu_container width_24px flex_justify_center margin_0 padding_0"> <nav id="dir_menu_main_container_nav" class="invert pointer width_14px_contents" title="Show main menu (&#8984;E); navigate by arrow keys or typed string."><div class="display_flex">${ SVG_UI_Icons.menu }</div></nav> <ul id="sidebar_menu_main" class="menu position_absolute background_grey_80 border_top border_bottom margin_0 padding_0 display_none box_shadow_menu position_LR_0 z_index_9998"> ${ sidebar_header_menu_elements } </ul> </div> </div>`;
  438. sidebar_texteditor_element = `<ul id="show_texteditor" class="bold border_bottom display_none"><li class="padding_4_6 width_100 background_grey_80" title="Toggle Text Editor (&#8984;&#8679;E)"><span>Text Editor</span></li></ul>`; break;
  439. case 'iframe': sidebar_header_menus = `<ul id="change_dirs" class="flex_justify_center_row flex_justify_contents border_bottom background_grey_75"> <li id="parent" class="flex_grow_1 no_highlight"><a href="${ parent_link }" id="iframe_parent_link" class="display_inline_flex" title="Go to parent directory"><span class="width_14px_contents invert">${ SVG_UI_Icons.chevron }</span>Parent Directory</a></li> <li id="open_in_sidebar" class="align_right flex_grow_1 no_highlight"><a href="#" title="Open this directory in sidebar">Open in Sidebar<span class="width_14px_contents invert transform_rotate_270_contents">${ SVG_UI_Icons.chevron }</span></a></li> </ul>`; break;
  440. }
  441. return `<header id="sidebar_header" class="display_flex flex_column text_color_default font_size_small user_select_none z_index_3"> ${ sidebar_header_title_element }
  442. <div id="sidebar_header_utilities" class="display_flex flex_column"> ${ sidebar_header_menus } ${ sidebar_header_utilities_row_1 } ${ sidebar_header_utilities_row_2 } ${ sidebar_texteditor_element } </div>
  443. </header>`;
  444. }
  445. let sidebar_footer_utilities = '', sidebar_utilities = '';
  446. if ( body_id === 'top_body' ) { // various elements not needed in iframe directories
  447. sidebar_footer_utilities = `<div id="sidebar_footer_utilities" class="width_24px flex_justify_center position_relative pointer z_index_1 background_grey_80 border_left border_right"><div class="width_18px_contents display_flex transform_rotate_180"><span class="invert">${ SVG_UI_Icons.toggle }</span></div> <ul class="has_popout_menu margin_0 padding_0 display_none border_all position_absolute background_grey_80"> <li id="open_in_content_pane" class="align_right border_bottom padding_4_6">Open Sidebar in Content Pane</li> <li id="show_directory_source" class="align_right padding_4_6" data-kind="show_directory_source">View Sidebar Directory Source</li> </ul> </div>`;
  448. sidebar_utilities = `<div id="sidebar_utilities"><div id="handle" class="position_absolute z_index_1"></div> <div id="show_sidebar" class="toggle_UI_pref width_24px width_18px_contents position_absolute flex_justify_center invert pointer z_index_9997" data-ui_pref="show_sidebar" title="Toggle Sidebar (&#8984;\\)">${ SVG_UI_Icons.toggle }</div></div> </div>`;
  449. }
  450. const sidebar_nav = `<nav id="sidebar_nav" class="display_flex flex_column background_grey_85 font_size_small"><div id="dir_nav_inner" class="position_relative"> <div id="directory_list_outer" class="position_relative"><ol id="directory_list" class="display_flex flex_column margin_0 padding_0 text_color_default border_bottom" tabindex="0">insert_prepped_index</ol></div> </div></nav>`;
  451. const sidebar_footer = `<footer id="sidebar_footer" class="display_flex flex_row position_relative background_grey_85 border_top text_color_default error_display_none font_size_small user_select_none">insert_stats${ sidebar_footer_utilities }</footer>`;
  452. return `<div id="sidebar" class="${body_id} display_flex flex_column position_relative border_right padding_0 z_index_1" style="width:${ Number(getCurrentUIPref("width")) }%"> ${ sidebar_header_elements(body_id,parent_link) } ${ sidebar_nav } ${ sidebar_footer } ${ sidebar_utilities } </div>`;
  453. }
  454. //==============================//
  455. // CONTENT PANE ELEMENTS
  456. function Content_Pane_Elements(id) {
  457. const content_audio_elements = `
  458. <div id="content_audio_title" class="flex_justify_center_row background_grey_80 bold align_center" title="Click to toggle .m3u playlist entry."><span class="pointer line_height_1_4"></span></div>
  459. <div id="content_audio" class="display_flex flex_row border_bottom background_grey_80">
  460. <div id="audio_container" class="display_flex flex_row border_all">
  461. <nav id="cuesheet_track_list_container_audio" class="cuesheet_track_list_container border_right" title="Cue sheet track list"> <div class="box_shadow_menu display_none font_size_small position_absolute position_LR_0 z_index_1"><ul id="cuesheet_track_list_audio" class="cuesheet_track_list background_grey_85 border_bottom margin_0 padding_0"> </ul></div> </nav>
  462. <div id="prev_track" class="prev_next_btn audio_controls flex_justify_center pointer" title="Previous track"><div class="display_flex width_24px_contents transform_rotate_180">${ SVG_UI_Icons.prev_next_track }</div></div> <div id="next_track" class="prev_next_btn audio_controls flex_justify_center border_right pointer" title="Next track"><div class="display_flex width_24px_contents">${ SVG_UI_Icons.prev_next_track }</div></div>
  463. <audio id="audio" class="outline_none" preload="auto" tabindex="0" controls>Sorry, your browser does not support HTML5 audio.</audio>
  464. <div id="close_audio" class="audio_controls border_left flex_justify_center position_relative pointer" title="Close audio"><div class="display_flex width_14px_contents">${ SVG_UI_Icons.multiply }</div></div>
  465. <div id="audio_options" class="display_flex flex_column"> <label id="loop_label" for="loop"><input type="checkbox" id="loop" name="loop" tabindex="0" />Loop</label> <label id="shuffle_label" class="whitespace_pre" for="shuffle"><input type="checkbox" id="shuffle" name="shuffle" tabindex="0" />Shuffle</label> </div>
  466. </div>
  467. </div>
  468. <div id="content_audio_playlist_item" class="playlist_entry_container border_bottom background_grey_85 align_center display_none"><textarea id="content_audio_playlist_item_textarea" class="text_color_default padding_4_6 border_0 outline_none" rows="3" spellcheck="false"></textarea></div>`;
  469. const text_editing_ui_elements = `
  470. <div id="texteditor_toolbar" class="border_bottom background_grey_80 position_relative text_color_default display_flex user_select_none width_100">
  471. <ul id="toolbar_buttons" class="display_flex flex_row flex_grow_1 margin_0 padding_0"> <li id="toggle_texteditor_view_raw" class="toggle_UI_pref toolbar_icon display_flex no_highlight" data-ui_pref="texteditor_view_raw" title="Show source"><div class="display_flex width_16px_contents invert">${ SVG_Text_Editing_UI_Icons.show_markdown }</div></li> <li id="toggle_texteditor_view_styled" class="toggle_UI_pref toolbar_icon display_flex no_highlight" data-ui_pref="texteditor_view_styled" title="Show rendered markdown"><div class="display_flex width_14px_contents invert">${ SVG_Text_Editing_UI_Icons.show_preview }</div></li> <li id="toggle_texteditor_view_html" class="toggle_UI_pref toolbar_icon display_flex no_highlight" data-ui_pref="texteditor_view_html" title="Show formatted HTML"><div class="display_flex width_18px_contents invert">${ SVG_Text_Editing_UI_Icons.show_html }</div></li> <li id="toggle_texteditor_split_view" class="toggle_UI_pref toolbar_icon display_flex no_highlight" data-ui_pref="texteditor_split_view" title="Toggle Split View"><div class="display_flex width_14px_contents invert">${ SVG_Text_Editing_UI_Icons.toggle_split }</div></li> <li id="texteditor_sync_scroll" class="toggle_UI_pref checkbox_container flex_justify_center_row no_highlight" data-ui_pref="texteditor_sync_scroll"><input id="texteditor_sync_scroll_input" class="toggle_UI_pref flex_justify_center_row position_relative" data-ui_pref="texteditor_sync_scroll" name="texteditor_sync_scroll" type="checkbox"><label id="texteditor_sync_scroll_label" for="texteditor_sync_scroll" class="toggle_UI_pref flex_justify_center_row whitespace_pre" data-ui_pref="texteditor_sync_scroll">Sync Scroll</label></li> <li class="display_flex flex_grow_1 no_highlight">&nbsp;</li> <li id="clear_text" class="toolbar_icon no_highlight" title="Clear Text">Clear</li> <li id="save_btn" class="has_flyout_menu width_24px display_flex position_relative pointer margin_0 padding_0 z_index_9997" title=""><div class="display_flex width_14px_contents">${ SVG_Text_Editing_UI_Icons.save_btn}</div><ul class="menu has_popout_menu display_none position_absolute margin_0 padding_0 box_shadow_menu border_top border_bottom border_left background_grey_80"> <li id="save_text" class="item_1 border_right border_bottom background_grey_85" title="Save source text"><span id="save_text_link" target="_blank">Save Source</span></li> <li id="save_btn_icon" class="item_1 no_highlight">${ SVG_Text_Editing_UI_Icons.save_btn}</li> <li id="save_HTML" class="item_2 border_right background_grey_85" title="Save rendered html"><span id="save_HTML_link" target="_blank">Save HTML</span></li> </ul></li> </ul> </div>
  472. <div id="text_container" class="display_flex flex_grow_1 overflow_hidden"> <textarea id="texteditor_raw_pane" class="texteditor_pane margin_0 border_0 height_100 line_height_1_2 text_color_default resize_none display_none z_index_1 outline_none" tabindex="0"></textarea> <div id="texteditor_styled_pane" class="texteditor_pane margin_0 border_0 line_height_1_2 text_color_default height_100 display_none markdown_body z_index_1" tabindex="0"></div> <textarea id="texteditor_html_pane" class="texteditor_pane margin_0 height_100 line_height_1_2 border_0 text_color_default resize_none display_none z_index_1 outline_none" tabindex="0" readonly></textarea> <div id="text_editing_handle" class="position_absolute z_index_3"></div> </div>
  473. `;
  474. const content_text_elements = `<div id="content_texteditor" class="background_grey_85 margin_0 padding_0 width_100 height_100 overflow_hidden position_absolute z_index_1 flex_column flex_grow_1 display_none">${ text_editing_ui_elements }</div>`;
  475. const content_font_toolbar = `<div id="font_toolbar" class="display_none margin_0 position_relative background_grey_80 border_bottom user_select_none z_index_3">
  476. <ol id="font_specimen_variants" class="display_none flex_row flex_grow_1 border_bottom"><li id="font_variants" class="flex_justify_center no_highlight" title="Font Variants"><select id="font_variant_select" data-tab_order="10" label="Font Variants">
  477. <option value="">OpenType Feature Tags</option>
  478. <optgroup label="Caps"> <option value="normal" data-prop="font-variant-caps" data-value="normal">Normal </option> <option value="smcp" title="smcp" data-prop="font-variant-caps" data-value="small-caps">Small Caps </option> <option value="c2sc" title="c2sc" data-prop="font-variant-caps" data-value="all-small-caps">All Small Caps </option> <option value="pcap" title="pcap" data-prop="font-variant-caps" data-value="petite-caps">Petite Caps </option> <option value="c2pc" title="c2pc" data-prop="font-variant-caps" data-value="all-petit-caps">All Petite Caps </option> <option value="unic" title="unic" data-prop="font-variant-caps" data-value="unicase">Unicase </option> <option value="titl" title="titl" data-prop="font-variant-caps" data-value="titling-caps">Titling Caps </option> <option value="case" title="case">Case Sensitive Forms </option> <option value="ordn" title="ordn">Ordinals </option> </optgroup>
  479. <optgroup label="Alternatives"> <option value="normal" data-prop="font-variant-alternates" data-value="normal">Normal </option> <option value="aalt" title="aalt">Access All Alternates </option> <option value="nalt" title="nalt" data-prop="font-variant-alternates" data-value="'nalt'">Annotation 1-99 </option> <option value="cv01" title="cv01" data-prop="font-variant-alternates" data-value="\"cv01\"">Character Variant 1–99 </option> <option value="calt" title="calt" data-prop="font-variant-alternates" data-value="cv01">Contextual Alts 1-99 </option> <option value="hist" title="hist">Historical Forms </option> <option value="ornm" title="ornm" data-prop="font-variant-alternates" data-value="ornm">Ornaments 1-99 </option> <option value="salt" title="salt" data-prop="font-variant-alternates" data-value="salt">Stylistic Alternates 1-99 </option> <option value="ss01" title="ss01" data-prop="font-variant-alternates" data-value="ss01">Stylistic Set 120 </option> <option value="swsh" title="swsh" data-prop="font-variant-alternates" data-value="\"swsh\"">Swash 1-99 </option> <option value="cswh" title="cswh">Contextual Swash </option> </optgroup>
  480. <optgroup label="Ligatures"> <option value="normal" data-prop="font-variant-ligatures" data-value="normal">Normal </option> <option value="liga" title="liga" data-prop="font-variant-ligatures" data-value="common-ligatures">Common Ligatures </option> <option value="clig" title="clig" data-prop="font-variant-ligatures" data-value="contextual">Contextual Ligatures </option> <option value="dlig" title="dlig" data-prop="font-variant-ligatures" data-value="discretionary-ligatures">Discretionary Ligatures </option> <option value="hlig" title="hlig" data-prop="font-variant-ligatures" data-value="historical-ligatures">Historical Ligatures </option> </optgroup>
  481. <optgroup label="Numbers"> <option value="normal" data-prop="font-variant-numeric" data-value="normal">Normal </option> <option value="dnom" title="dnom" data-prop="font-variant-numeric" data-value="ordinal">Ordinal </option> <option value="zero" title="zero" data-prop="font-variant-numeric" data-value="slashed-zero">Slashed Zero </option> <option value="lnum" title="lnum" data-prop="font-variant-numeric" data-value="lining-nums">Lining Figures </option> <option value="onum" title="onum" data-prop="font-variant-numeric" data-value="oldstyle-nums">Oldstyle Figures </option> <option value="pnum" title="pnum" data-prop="font-variant-numeric" data-value="proportional-nums">Proportional Figures </option> <option value="tnum" title="tnum" data-prop="font-variant-numeric" data-value="tablular-nums">Tabular Figures </option> <option value="frac" title="frac" data-prop="font-variant-numeric" data-value="diagonal-fractions">Fractions </option> <option value="afrc" title="afrc" data-prop="font-variant-numeric" data-value="stacked-fractions">Alternative Fractions </option> <option value="numr" title="numr">Numerator </option> <option value="sinf" title="sinf">Scientific Inferiors </option> <option value="mgrk" title="mgrk">Mathematical Greek </option> </optgroup>
  482. <optgroup label="Position"> <option value="normal" data-prop="font-variant-position" data-value="normal">Normal </option> <option value="subs" title="subs" data-prop="font-variant-position" data-value="sub">Subscript </option> <option value="sups" title="sups" data-prop="font-variant-position" data-value="super">Superscript </option> </optgroup>
  483. </select></li>
  484. <li id="font_tag" class="flex_justify_center no_highlight" data-salt=""><span class="flex_justify_center position_relative"><textarea id="font_tag_textarea" data-tab_order="12" class="resize_none outline_none" rows="1" cols="5" spellcheck="false" maxlength="8" placeholder="otftag" title="Enter an OpenType Feature Tag (e.g.: &ldquo;smcp&rdquo;)"></textarea></span></li>
  485. <li class="spacer no_highlight width_100"></li>
  486. <li id="unicode_char_planes" class="flex_justify_center no_highlight" title="Unicode Code Ranges"><select id="unicode_char_ranges_select" data-tab_order="13">
  487. <option value="">Unicode Code Ranges</option>
  488. <optgroup label="Basic Multilingual Plane"> <option id="BMP_Range_01" value="BMP_Range_01" data-block_start="0000" data-block_end="0FFF">BMP-01: U+0000U+0FFF</option> <option id="BMP_Range_02" value="BMP_Range_02" data-block_start="1000" data-block_end="1FFF">BMP-02: U+1000–U+1FFF</option> <option id="BMP_Range_03" value="BMP_Range_03" data-block_start="2000" data-block_end="2FFF">BMP-03: U+2000U+2FFF</option> <option id="BMP_Range_04" value="BMP_Range_04" data-block_start="3000" data-block_end="3FFF">BMP-04: U+3000–U+3FFF</option> <option id="BMP_Range_05" value="BMP_Range_05" data-block_start="4000" data-block_end="4FFF">BMP-05: U+4000U+4FFF</option> <option id="BMP_Range_06" value="BMP_Range_06" data-block_start="5000" data-block_end="5FFF">BMP-06: U+5000–U+5FFF</option> <option id="BMP_Range_07" value="BMP_Range_07" data-block_start="6000" data-block_end="6FFF">BMP-07: U+6000U+6FFF</option> <option id="BMP_Range_08" value="BMP_Range_08" data-block_start="7000" data-block_end="7FFF">BMP-08: U+7000–U+7FFF</option> <option id="BMP_Range_09" value="BMP_Range_09" data-block_start="8000" data-block_end="8FFF">BMP-09: U+8000U+8FFF</option> <option id="BMP_Range_10" value="BMP_Range_10" data-block_start="9000" data-block_end="9FFF">BMP-10: U+9000–U+9FFF</option> <option id="BMP_Range_11" value="BMP_Range_11" data-block_start="A000" data-block_end="AFFF">BMP-11: U+A000U+AFFF</option> <option id="BMP_Range_12" value="BMP_Range_12" data-block_start="B000" data-block_end="BFFF">BMP-12: U+B000–U+BFFF</option> <option id="BMP_Range_13" value="BMP_Range_13" data-block_start="C000" data-block_end="CFFF">BMP-13: U+C000U+CFFF</option> <option id="BMP_Range_14" value="BMP_Range_14" data-block_start="D000" data-block_end="DFFF">BMP-14: U+D000–U+DFFF</option> <option id="BMP_Range_15" value="BMP_Range_15" data-block_start="E000" data-block_end="EFFF">BMP-15: U+E000U+EFFF</option> <option id="BMP_Range_16" value="BMP_Range_16" data-block_start="F000" data-block_end="FFFF">BMP-16: U+F000–U+FFFF</option> </optgroup>
  489. <optgroup label="Supplementary Multilingual Plane" data-block_start="10000" data-block_end="10FFF"> <option id="SMP_Range_01" value="SMP_Range_01">SMP-01: U+10000U+10FFF</option> <option id="SMP_Range_02" value="SMP_Range_02" data-block_start="11000" data-block_end="11FFF">SMP-02: U+11000–U+11FFF</option> <option id="SMP_Range_03" value="SMP_Range_03" data-block_start="12000" data-block_end="12FFF">SMP-03: U+12000U+12FFF</option> <option id="SMP_Range_04" value="SMP_Range_04" data-block_start="13000" data-block_end="13FFF">SMP-04: U+13000–U+13FFF</option> <option id="SMP_Range_05" value="SMP_Range_05" data-block_start="14000" data-block_end="14FFF">SMP-05: U+14000U+14FFF</option> <option id="SMP_Range_06" value="SMP_Range_06" data-block_start="15000" data-block_end="15FFF">SMP-06: U+15000–U+15FFF</option> <option id="SMP_Range_07" value="SMP_Range_07" data-block_start="16000" data-block_end="16FFF">SMP-07: U+16000U+16FFF</option> <option id="SMP_Range_08" value="SMP_Range_08" data-block_start="17000" data-block_end="17FFF">SMP-08: U+17000–U+17FFF</option> <option id="SMP_Range_09" value="SMP_Range_09" data-block_start="18000" data-block_end="18FFF">SMP-09: U+18000U+18FFF</option> <option id="SMP_Range_10" value="SMP_Range_10" data-block_start="19000" data-block_end="19FFF">SMP-10: U+19000–U+19FFF</option> <option id="SMP_Range_11" value="SMP_Range_11" data-block_start="1A000" data-block_end="1AFFF">SMP-11: U+1A000U+1AFFF</option> <option id="SMP_Range_12" value="SMP_Range_12" data-block_start="1B000" data-block_end="1BFFF">SMP-12: U+1B000–U+1BFFF</option> <option id="SMP_Range_13" value="SMP_Range_13" data-block_start="1C000" data-block_end="1CFFF">SMP-13: U+1C000U+1CFFF</option> <option id="SMP_Range_14" value="SMP_Range_14" data-block_start="1D000" data-block_end="1DFFF">SMP-14: U+1D000–U+1DFFF</option> <option id="SMP_Range_15" value="SMP_Range_15" data-block_start="1E000" data-block_end="1EFFF">SMP-15: U+1E000U+1EFFF</option> <option id="SMP_Range_16" value="SMP_Range_16" data-block_start="1F000" data-block_end="1FFFF">SMP-16: U+1F000–U+1FFFF</option> </optgroup>
  490. <optgroup label="Supplementary Ideographic Plane" data-block_start="20000" data-block_end="20FFF"> <option id="SIP_Range_01" value="SIP_Range_01">SIP-01: U+20000U+20FFF</option> <option id="SIP_Range_02" value="SIP_Range_02" data-block_start="21000" data-block_end="21FFF">SIP-02: U+21000–U+21FFF</option> <option id="SIP_Range_03" value="SIP_Range_03" data-block_start="22000" data-block_end="22FFF">SIP-03: U+22000U+22FFF</option> <option id="SIP_Range_04" value="SIP_Range_04" data-block_start="23000" data-block_end="23FFF">SIP-04: U+23000–U+23FFF</option> <option id="SIP_Range_05" value="SIP_Range_05" data-block_start="24000" data-block_end="24FFF">SIP-05: U+24000U+24FFF</option> <option id="SIP_Range_06" value="SIP_Range_06" data-block_start="25000" data-block_end="25FFF">SIP-06: U+25000–U+25FFF</option> <option id="SIP_Range_07" value="SIP_Range_07" data-block_start="26000" data-block_end="26FFF">SIP-07: U+26000U+26FFF</option> <option id="SIP_Range_08" value="SIP_Range_08" data-block_start="27000" data-block_end="27FFF">SIP-08: U+27000–U+27FFF</option> <option id="SIP_Range_09" value="SIP_Range_09" data-block_start="28000" data-block_end="28FFF">SIP-09: U+28000U+28FFF</option> <option id="SIP_Range_10" value="SIP_Range_10" data-block_start="29000" data-block_end="29FFF">SIP-10: U+29000–U+29FFF</option> <option id="SIP_Range_11" value="SIP_Range_11" data-block_start="2A000" data-block_end="2AFFF">SIP-11: U+2A000U+2AFFF</option> <option id="SIP_Range_12" value="SIP_Range_12" data-block_start="2B000" data-block_end="2BFFF">SIP-12: U+2B000–U+2BFFF</option> <option id="SIP_Range_13" value="SIP_Range_13" data-block_start="2C000" data-block_end="2CFFF">SIP-13: U+2C000U+2CFFF</option> <option id="SIP_Range_14" value="SIP_Range_14" data-block_start="2D000" data-block_end="2DFFF">SIP-14: U+2D000–U+2DFFF</option> <option id="SIP_Range_15" value="SIP_Range_15" data-block_start="2E000" data-block_end="2EFFF">SIP-15: U+2E000U+2EFFF</option> <option id="SIP_Range_16" value="SIP_Range_16" data-block_start="2F000" data-block_end="2FFFF">SIP-16: U+2F000–U+2FFFF</option> </optgroup>
  491. <optgroup label="Tertiary Ideographic Plane" data-block_start="30000" data-block_end="30FFF"> <option id="TIP_Range_01" value="TIP_Range_01">TIP-01: U+30000U+30FFF</option> <option id="TIP_Range_02" value="TIP_Range_02" data-block_start="31000" data-block_end="31FFF">TIP-02: U+31000–U+31FFF</option> </optgroup>
  492. <optgroup label="Supplementary Special-Purpose Plane" data-block_start="E0000" data-block_end="E0FFF"> <option id="SSP_Range_01" value="SSP_Range_01">SSP-01: U+E0000U+E0FFF</option> </optgroup>
  493. </select></li>
  494. </ol>
  495. <ol id="font_specimen_adjustments" class="display_flex flex_row flex_grow_1">
  496. <li class="no_highlight" data-inputid="font_size"><span class="flex_justify_center"><input type="range" id="font_size" data-tab_order="14" name="font_size" min="0" max="2" step="any"><label for="font_size">Font Size</label></span></li> <li class="no_highlight display_none" data-inputid="line_height"><span class="flex_justify_center"><input type="range" id="line_height" data-tab_order="15" name="line_height" min="-1.2" max="1.2" step="any"><label for="line_height">Line Height</label></span></li> <li class="no_highlight display_none" data-inputid="letter_spacing"><span class="flex_justify_center"><input type="range" id="letter_spacing" data-tab_order="16" name="letter_spacing" min="-100" max="100" step="1"><label for="letter_spacing">Letter Spacing</label></span></li>
  497. <li class="spacer no_highlight width_100"></li>
  498. <li data-inputid="text_color" class="display_flex no_highlight"><span class="flex_justify_center position_relative"><textarea id="text_color" class="whitespace_pre resize_none outline_none" data-tab_order="17" rows="1" cols="7" spellcheck="false" placeholder="CSS color"></textarea> Text Color</span></li> <li data-inputid="text_stroke_width" class="display_flex no_highlight"><span class="flex_justify_center"><input type="range" id="text_stroke_width" data-tab_order="18" name="font_size" min="-0.5" max="0.5" step="any" title="In supported browsers only."><label for="text_stroke_width">Text Stroke Width</label></span></li> <li data-inputid="text_stroke_color" class="display_flex no_highlight"><span class="flex_justify_center position_relative"><textarea id="text_stroke_color" class="whitespace_pre resize_none outline_none" data-tab_order="19" rows="1" cols="7" spellcheck="false" placeholder="CSS color"></textarea>Text Stroke Color</span></li>
  499. </ol></div>`;
  500. const content_font_sample_string = `ABCDEFGHIJKLMNOPQRSTUVWXYZ<br />abcdefghijklmnopqrstuvwxyz<br />0123456789<br />!"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`;
  501. const content_font_lorem_string = `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
  502. const content_font_viewer = '<div id="font_file_viewer" class="position_absolute position_0 display_none"><div id="font_file_glyph_viewer" class="invert margin_0 padding_0 position_absolute position_0 display_none z_index_2"></div><ol id="font_file_grid" class="font_grid position_relative padding_0 align_center display_grid overflow_x_hidden"></ol></div>';
  503. const content_font_elements = `<div id="font_specimen_viewer" class="padding_0 display_none">
  504. <ol id="font_specimen_grid" class="font_grid display_grid overflow_visible"></ol>
  505. <div id="font_specimen"> <div id="font_specimen_1" class="specimen border_bottom_x margin_0 overflow_visible normal" data-tab_order="1" contenteditable="true">${ content_font_sample_string }</div> <div id="font_specimen_2" class="specimen border_bottom_x align_left overflow_visible normal" data-tab_order="2" contenteditable="true" tabindex="0"><h2 id="specimen_2" class="margin_0 line_height_1 normal">Typography</h2><p id="specimen_2H4">The art of using types to produce impressions on paper, vellum, &amp;c.</p></div> <div id="font_specimen_3" class="specimen border_bottom_x align_justify overflow_visible normal line_height_1" data-tab_order="3" contenteditable="true"><h3 id="specimen_3" class="margin_0 normal">S P E C I M E N</h3><p id="specimen_3H3" class="margin_0">Typography is the work of typesetters (also known as compositors), typographers, graphic designers, art directors, manga artists, comic book artists, graffiti artists, and, now, anyone who arranges words, letters, numbers, and symbols for publication, display, or distribution.</p></div> <div id="font_specimen_4" class="specimen overflow_visible normal" data-tab_order="4" contenteditable="true" tabindex="0"> <div id="lorem" class="lorem align_justify">${ content_font_lorem_string }</div><div id="lorem_2" class="lorem align_justify">${ content_font_lorem_string }</div><div id="lorem_3" class="lorem align_justify">${ content_font_lorem_string }</div></div></div>
  506. <div id="font_specimen_glyph_viewer" class="background_grey_90 display_none overflow_visible normal"><div id="font_specimen_glyph" class="flex_justify_center position_fixed position_0 z_index_2" data-scale="1"></div><div id="font_specimen_glyph_overlay" class="position_fixed position_0 background_grey_100 user_select_none z_index_1"></div></div>
  507. </div> ${ content_font_viewer }`;
  508. const content_header_elements = `
  509. <header id="content_header" class="font_size_small z_index_3">
  510. <div id="audio_wrapper" class="text_color_default background_grey_75 display_none"> ${ content_audio_elements } </div>
  511. <div id="content_title_container" class="title display_flex text_color_default border_bottom">
  512. <div id="title_buttons_left" class="display_flex padding_4_6 align_left">
  513. <nav id="cuesheet_track_list_container_video" class="cuesheet_track_list_container background_grey_75" title="Cue sheet track list"><div class="box_shadow_menu display_none font_size_small position_absolute position_LR_0 z_index_1"><ul id="cuesheet_track_list_video" class="cuesheet_track_list border_bottom margin_0 padding_0 display_none"></ul></div></nav>
  514. <button id="reload_btn" class=" outline_none" tabindex="-1"><span></span></button> <button id="prev_next_btns" class="split_btn padding_0 position_relative display_none outline_none" tabindex="-1"><span id="prev_btn" class="prev_next_btn flex_justify_center"><span class="transform_rotate_270_contents">${ SVG_UI_Icons.chevron }</span></span><span id="next_btn" class="prev_next_btn flex_justify_center"><span class="display_flex transform_rotate_90_contents">${ SVG_UI_Icons.chevron }</span></span></button>
  515. </div>
  516. <div id="content_title" class="pointer align_center hyphens_none line_height_1_4 flex_justify_center_row flex_grow_1"><div><span class="has_icon_before has_icon_after bold"></span></div></div>
  517. <div id="title_buttons_right" class="display_flex padding_4_6 align_right"> <button id="scale" class="split_btn padding_0 position_relative display_none outline_none" tabindex="-1"><span id="decrease" class="flex_justify_center" title="Reduce"><div class="display_flex width_10px_contents">${ SVG_UI_Icons.minus }</div></span><span id="increase" class="flex_justify_center" title="Enlarge"><div class="display_flex width_10px_contents">${ SVG_UI_Icons.plus }</div></span></button> <button id="open_in_texteditor" class="display_none outline_none" title="Open in Text Editor" tabindex="-1"><span>Edit</span></button> <button id="save_svg" class="display_none whitespace_pre outline_none" title="Save glyph as svg"><span>Save SVG</span></button> <button id="close_btn" class="outline_none" tabindex="-1" title="Close Content"><span></span></button></div>
  518. </div>
  519. <div id="content_playlist_item" class="playlist_entry_container border_bottom background_grey_85 align_center display_none"><textarea id="content_playlist_item_textarea" class="text_color_default padding_4_6 border_0 outline_none" rows="3" spellcheck="false"></textarea></div>
  520. ${ content_font_toolbar }
  521. </header>`;
  522. switch(true) { // ASSEMBLE CONTENT ELEMENTS
  523. case id === 'content_font_viewer': return content_font_viewer;
  524. case id === 'content_text_elements': return content_text_elements;
  525. default: return `<div id="content_pane" class="display_flex flex_column flex_grow_1 position_relative padding_0" data-content="has_null"> ${ content_header_elements } <main id="content_container" class="display_flex position_relative background_grey_90 no_hover margin_0 padding_0"> ${ SVG_UI_Icons.spinner } <ol id="content_grid" class="content_el" data-kind="grid"></ol> ${ content_text_elements } <div id="content_font" class="content_el background_grey_90 hyphens_none position_relative text_color_default" spellcheck="false" data-kind="font">${ content_font_elements }</div> <div id="content_image_container" class="content_el background_grey_95 position_relative margin_0" data-kind="image"><img id="content_image" class="content_el position_relative" src="#" alt="" tabindex="0" /></div> <embed id="content_pdf" class="content_el position_relative border_0" tabindex="0" data-kind="pdf"> <video id="content_video" class="content_el background_grey_95 media position_absolute" controls data-kind="video">Your browser does not support the video tag.</video> <iframe id="content_iframe" class="content_el position_relative border_0" name="content_iframe" sandbox="allow-scripts allow-same-origin allow-modals allow-popups" tabindex="0"></iframe> <iframe id="content_iframe_utility" class="display_none" name="content_iframe_utility" sandbox="allow-scripts allow-same-origin allow-modals allow-popups" tabindex="0"></iframe> </main></div>`;
  526. }
  527. }
  528. //==============================//
  529. // UTILITIES HTML (warnings and help)
  530. function Utilities_Elements(body_id) {
  531. let utilities_warning_elements = `<header id="warnings_header" class="text_color_default background_grey_85"><h3 id="warning_header" class="display_none margin_0"><span>Warning:</span></h3><h3 id="make_playlist_header" class="display_none margin_0 normal"><span>Make Playlist/Filelist (.m3u)</span></h3></header>
  532. <div id="warnings" class="text_color_default background_grey_85">
  533. <div id="warning_open_font" class="warning">Are you sure you want to close the font file?</div> <div id="warning_close_font" class="warning">Are you sure you want to close the font file?</div> <div id="warning_unsaved_text" class="warning">You have unsaved changes.</div> <div id="warning_clear_text" class="warning">Are you sure you want to clear all your text?</div> <div id="warning_local_file" class="warning">Can&rsquo;t load local file from non-local page.</div> <div id="warning_close_playlist" class="warning">Are you sure you want to close the playlist?</div> <div id="warning_local_playlist" class="warning">This playlist contains local files. <br />&emsp;Please reload this playlist from a local page in order to play them.</div> <div id="warning_non_local_file" class="warning">This is a non-local file/dir/link. Would you like to open it in a new window?</div>
  534. <div id="warning_make_playlist" class="warning"> <form id="make_playlist_form" action="#"><fieldset class="margin_0 padding_0 border_0"> <div><input name="make_playlist" type="radio" id="all_items" checked><label for="all_items">All items</label></div> <div class="indent"><input name="make_playlist" type="radio" id="directories_only"><label for="directories_only">Directories only</label></div> <div class="indent"><input name="make_playlist" type="radio" id="files_only"><label for="files_only">Files only</label></div> <div><input name="make_playlist" type="radio" id="media_files_only"><label for="media_files_only">All media files</label></div> <div class="indent"><input name="make_playlist" type="radio" id="audio_files_only"><label for="audio_files_only">Audio files only</label></div> <div class="indent"><input name="make_playlist" type="radio" id="video_files_only"><label for="video_files_only">Video files only</label></div> <div><input name="make_playlist" type="radio" id="all_non_media_files"><label for="all_non_media_files">All non-media items</label></div> </fieldset></form> </div>
  535. </div>
  536. <div id="warning_buttons_container" class="display_flex flex_column background_grey_90"> <div id="warning_buttons" class="display_flex flex_row"> <button id="warning_btn_dont_save" class="warning_button">Don&rsquo;t Save</button> <button id="warning_btn_cancel" class="warning_button">Cancel</button> <button id="warning_btn_clear" class="warning_button">Clear</button> <button id="warning_btn_save" class="warning_button">Save</button> <button id="warning_btn_ok" class="warning_button">OK</button> </div> </div>`;
  537. let utilities_help_elements = `<header id="help_header" class="title display_grid padding_4_6 position_LR_0 text_color_default border_bottom_x background_grey_75 align_center position_fixed z_index_3"><span class="spacer"></span><span class="bold flex_justify_center">HELP</span><button id="close_help" class="focus outline_none"><span>Close</span></button></header><section class="line_height_1_4">
  538. <ol style="margin:1em 3em 0;list-style-type:decimal;" class="no_highlight bold">CONTENTS<li class="no_highlight">I. About this Script</li><li class="no_highlight">II. Keyboard Shortcuts</li><li class="no_highlight">III. Usage: The Main Menu</li><li class="no_highlight">IV. Other Script Functions</li><li class="no_highlight">V. Troubleshooting</li></ol>
  539. <h2 class="invert"><strong>I. ABOUT THIS SCRIPT</strong></h2>
  540. <div class="invert"><p>This script works &ldquo;out-of-the-box&rdquo; on local directories. To enable it to work on a remote directory, add its url to the list of included/allowed sites in your userscript manager&rsquo;s settings for this script.</p>
  541. <p class="bold invert"><a href="https://openuserjs.org/scripts/gaspar_schot/Supercharged_Local_Directory_File_Browser" class="has_icon_before link text_color_default" target="_blank">Script home: openuserjs.org</a></p>
  542. <p><span style="float:left; margin:4px 6px 0 0;">${ SVG_UI_Icons.ui_layout }</span>The UI consists of two main parts: (1) the directory list <strong>Sidebar</strong> on the left and (2) the <strong>Content Pane</strong> on the right. The Sidebar shows all the items in the current directory, while the Content Pane shows a preview of items selected in the Sidebar.</p>
  543. <p>The <strong>Sidebar</strong> comprises a <strong>Header</strong> area, the <strong>Directory List</strong> itself, and a <strong>Footer</strong>.</p>
  544. <p>The <strong>Sidebar</strong> is resizeable; it can be hidden completely by clicking the double-chevron icon at the Sidebar top right or typing <b>&#8984;\</b>.</p>
  545. <p>The <strong>Sidebar Header</strong> includes a <strong>Parent Directory</strong> button, a <strong>Parent Directories</strong> menu, and the <strong>Main Menu</strong>, and below these, <strong>Show Details</strong> and <strong>Show Invisibles</strong> items, a <strong>Show Grid</strong> button (when appropriate), and sort by <strong>Name</strong> or <strong>Default</strong> items. If <strong>Show Details</strong> is selected, additional sorting options are shown, along with the <strong>Text Editor</strong> item. All of these items are also available in the Main Menu, and some can be toggled via keyboard shortcuts (see below).</p>
  546. <p> The <strong>Content Pane</strong> displays the selected sidebar item.</p>
  547. </div>
  548. <ul><li class="no_hover no_highlight">The content pane can be focused by tabbing from the sidebar or clicking.</li> <li class="no_hover no_highlight">Clicking the title of the content pane title reveals a playlist item than can be copied into an m3u file.</li> <li class="no_hover no_highlight">Previewed Content</li>
  549.  
  550. <dl><dt class="no_hover no_highlight">Directories<dt>
  551. <dd><Previewed directories in the Content Pane inherit the sorting and other UI preferences from the Sidebar directory list. They can be navigated independently from the Sidebar via the Parent Directory link in the header or Cmd-Up Arrow.</dd>
  552. <dd>An item in the content pane header allows previewed directories to be opened into the sidebar.</dd>
  553. <dd>A selected item can be previewed by pressing the spacebar. This is similar to the &ldquo;quicklook&rdquo; function in MacOS.</dd>
  554. <dd>Double-clicking a selected directory list item or typing Cmd-Down Arrow will open it in the content pane, replacing the previewed directory. Closing the item via the Close Button or Cmd-W will restore the original previewed directory.</dd>
  555. </dl>
  556.  
  557. <p class="invert">The <strong>Footer</strong> displays <strong>Stats</strong> for the items in the current directory, with detailed stats shown if you click the footer. There is also a popup menu on the right that allows you to display the Sidebar directory or the raw directory index in the Content Pane.</p>
  558.  
  559. <section><ul id="utilities_help" class="info_list background_grey_80 font_size_small border_all padding_0 no_highlight"> <li id="shortcuts" class="info_list_header display_grid align_center bold no_hover no_highlight"><span class="col_1">II. KEYBOARD SHORTCUTS</span><span class="col_2">DESCRIPTION</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&uarr;</kbd> or <kbd>&darr;</kbd></span><span class="col_2">Select the previous/next sidebar item or previewed directory item.<br />If audio is playing, and the previous/next file is also audio, the file will be highlighted but not loaded in the audio player; press <kbd>return</kbd> to load it.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&larr;</kbd> or <kbd>&rarr;</kbd></span><span class="col_2">Select prev/next item of the same kind as the current selection.<br />If current selection is a media file, select and begin playback of the next media item.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8997;</kbd><kbd>&larr;</kbd> or <kbd>&#8594;</kbd></span><span class="col_2">Skip media &plusmn;10 seconds.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8997;</kbd><kbd>&#8679;</kbd><kbd>&larr;</kbd> or <kbd>&rarr;</kbd></span><span class="col_2">Skip media &plusmn;30 seconds.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&uarr;</kbd></span><span class="col_2">Go to parent directory.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&darr;</kbd></span><span class="col_2">Go to selected sidebar directory.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&rarr;</kbd></span><span class="col_2">Open selected sidebar directory as subdirectory.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&larr;</kbd></span><span class="col_2">1. Close selected subdirectory, or <br />2. jump from selected subdirectory item to parent directory, or <br />3. jump up to closest open subdirectory, or <br />4. jump up to top of directory list.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>Escape</kbd></span><span class="col_2">Close menus and help, unfocus textareas and content pane, etc.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>Return</kbd></span><span class="col_2">Open selected sidebar directory, select file, or pause/play media.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>Space</kbd></span><span class="col_2">Pause/Play media files (if media player loaded).<br />&ldquo;Quicklook&rdquo; selected content pane directory list item.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>Tab</kbd></span><span class="col_2">Toggle focus between sidebar and content pane.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&#8679;</kbd><kbd>D</kbd></span><span class="col_2">Toggle file details (size, date modified, kind) in some index page types.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>E</kbd></span><span class="col_2">Toggle main menu.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&#8679;</kbd><kbd>E</kbd></span><span class="col_2">Show text editor.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>G</kbd></span><span class="col_2">Show or reload image or font grids.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&#8679;</kbd><kbd>I</kbd></span><span class="col_2">Toggle invisible files.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&#8679;</kbd><kbd>J</kbd></span><span class="col_2">Go to item by row number.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&#8679;</kbd><kbd>O</kbd></span><span class="col_2">Open selected sidebar item in new window/tab.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>R</kbd></span><span class="col_2">Reload grids and previewed content, reset scaled images/fonts, reset media files to beginning.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>W</kbd></span><span class="col_2">Close previewed content (doesn&rsquo;t work in all browsers; use close button instead), or close window if no content is being previewed.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>&#8679;</kbd><kbd>&lt;</kbd> or <kbd>&gt;</kbd></span><span class="col_2">Scale preview items and grids.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8984;</kbd><kbd>\\</kbd></span><span class="col_2">Toggle sidebar.</span></li> <li class="display_grid no_hover no_highlight"><span class="col_1"><kbd>&#8679;</kbd><kbd>&#8984;</kbd><kbd>\\</kbd></span><span class="col_2">Toggle text editor split view.</span></li> </ul></section>
  560.  
  561. <section class="invert"><h2 id="usage"><strong>III. USAGE</strong></h2>
  562. <div id="help_main_menu" class="border_top_x">
  563. <p><span class="width_14px_contents">${ SVG_UI_Icons.menu }</span><strong> MAIN MENU</strong> (&#8984;+E)<br />The Main Menu contains the following top-level items:<p>
  564. <ol style="margin:0 3em;list-style-type:decimal;" class="no_highlight bold invert"><li class="no_highlight">Go to item... (&#8984;+&#8679;+J)</li><li class="no_highlight" style="margin-top:6px;">Sort by... ${SVG_UI_Icons.arrow}</li><li class="no_highlight" style="margin-top:6px;">UI Preferences ${SVG_UI_Icons.arrow}</li><li class="no_highlight">File Handling Preferences ${SVG_UI_Icons.arrow}</li><li class="no_highlight">Media Preferences ${SVG_UI_Icons.arrow}</li><li class="no_highlight">Text Editing Preferences ${SVG_UI_Icons.arrow}</li><li class="no_highlight">Default Preferences</li><li class="no_highlight" style="margin-top:6px;">Playlists ${SVG_UI_Icons.arrow}</li><li class="no_highlight" style="margin-top:6px;">Open Font File...</li><li class="no_highlight" style="margin-top:6px;">Script Home</li><li class="no_highlight">Help</li><li class="no_highlight">Buy me a Coffee</li><li class="no_highlight">Contact</li></ol>
  565. <dl class="border_top_x padding_top_1rem"><dt>1. Go to Item...</dt> <dd>Select a sidebar item by its row number (displayed if "Show Numbers" pref is set). Useful especially for large directories. Directory list can also be navigated by typed strings.</dd></dl>
  566. <dl class="border_top_x padding_top_1rem padding_bottom_1rem"><dt>2. Sort By...</dt><dd>Sort directory items by <strong>Name</strong>, <strong>Size</strong>, <strong>Date</strong>, <strong>Kind</strong>, <strong>Extension</strong>, <strong>Duration</strong> (media items only). <strong>Default</strong> sort = sort items by Name, with directories on top.</dd>
  567. <dd>Clicking the sort preference again will reverse the sort.</dd>
  568. <dd>Sorting preferences are also available in the Sidebar Header.</dd>
  569. <dd>Note that many server configurations don't report directory size, so when sorting by size, directories will be on top, sorted by name.</dd>
  570. </dl>
  571. <p class="border_top_x"><strong>3. UI PREFERENCES</strong><br />Selecting these preferences items will add the new setting to the URL query string, so that the setting will persist as you navigate within the same window, or if you bookmark the page.</p>
  572. <dl><dt>a. Light Theme/Dark Theme</dt> <dd>Change the UI theme.</dd></dl>
  573. <dl><dt>b. Alternate Backgrounds</dt> <dd>Alternate background colors for directory list items.</dd></dl>
  574. <dl><dt>c. Show Numbers</dt> <dd>Show numbers for directory list items.</dd></dl>
  575. <dl><dt>d. Use Custom Icons</dt> <dd>If enabled, use custom file and directory icons provided by the script; if disabled, use the browser's default icons.</dd></dl>
  576. <dl><dt>e. Show Image Thumbnails</dt> <dd>Replace image file icons with a thumbnail of the image itself. Enabling this setting may slow down page load, because each image file in the directory must be downloaded.</dd></dl>
  577. <dl><dt>f. Use Large Image Thumbnails</dt> <dd>If "Show Image Thumbnails" is also enabled, a larger version of the image thumbnail is displayed.</dd></dl>
  578. <dl><dt>g. Always Show Image Thumbnails</dt> <dd>Image thumbnails are always shown, no matter how many images are in the directory. This overrides the default behavior which automatically disables thumbnail display for directories containing more than 2000 items in order to improve performance.</dd></dl>
  579. <dl><dt>h. Set UI Font</dt> <dd>Set a custom font for the UI.</dd></dl>
  580. <dl><dt>i. Scale UI</dt> <dd>Scale the entire UI (50%–150%). Double-click the label to quickly reset the scale to 100%.</dd></dl>
  581. <p class="border_top_x"><strong>4. FILE HANDLING PREFERENCES</strong></p>
  582. <dl><dt>a. Show/Hide Invisible Items</dt> <dd>It does what it says on the tin...</dd></dl>
  583. <dl><dt>b. Show/Hide Ignored Items</dt> <dd>Ignored items include files that the browser cannot handle natively (e.g., common Office and graphics files, various binary files, etc.). Hide them to reduce clutter.</dd>
  584. <dl><dt>c. Ignore Ignored Items</dt> <dd>Prevent normal browser behavior for handling such files, which is to open a download file dialog.</dd></dl>
  585. <dl><dt>d. Autoload Index Files</dt> <dd>Automatically load "index.html" or similar files in the Content Pane when the directory loads.</dd></dl>
  586. <p class="border_top_x"><strong>5. MEDIA PREFERENCES</strong></p>
  587. <dl><dt>a. Autoload Media</dt> <dd>If enabled, automatically load the first media file (audio or video) when the directory loads.</dd>
  588. <dd>For audio files, this will also automatically load any cover art (image file) found in the same directory. The script will first look for an image file with <i>exactly</i> the same name as the currently selected/playing audio file, followed in order by files containing the words cover”, front”, album”, jacket”, sleeve”, cd”, disc”, insert”, liner”, or notes.” If it finds no matching files, it will load the first image file it finds. Cover art will be automatically loaded whenever a new audio file is selected for playback.</dd></dl>
  589. <dl><dt>b. Autoplay Media</dt> <dd>If enabled, play the next media file when the currently playing media file ends.</dd></dl>
  590. <dl><dt>c. Play All Media Files</dt> <dd>If disabled (and Autoplay Media enabled), only play media of the same type (audio or video) as the currently playing media file.</dd></dl>
  591. <dl><dt>d. Loop Media Playback</dt> <dd>Loop media playback to the first media item when the last media item ends and continue playing. This option can also be enabled from the audio player.</dd></dl>
  592. <dl><dt>e. Shuffle Media Playback</dt> <dd>Randomize the order of media playback. This option can also be enabled from the audio player.</dd></dl>
  593. <p class="border_top_x"><strong>6. TEXT EDITING PREFERENCES</strong></p>
  594. <dl><dt>a. Text Editing Enabled/Disabled</dt><dd>If text editing is disabled, text files are displayed as normal files.</dd></dl>
  595. <dl><dt>b. Text Editing Options</dt><dd>Toggle the Text Editor. Select Editor UI theme: Default = same as main UI. Toggle split view. Select view of raw text, preview text, rendered HTML.</dd></dl>
  596. <dl class="border_top_x padding_top_1rem padding_bottom_1rem"><dt>7. DEFAULT PREFERENCES</dt><dd>Resets UI to defaults by removing manually set preferences from the URL query string.</dd></dl>
  597. <p class="border_top_x"><strong>8. PLAYLISTS</strong></p>
  598. <dl><dt>a. Open Playlist/Filelist</dt> <dd>Click to load a local .m3u file. See below for more details.</dd></dl>
  599. <dl><dt>b. Make Playlist/Filelist</dt><dd>Make an .m3u playlist/filelist of the items in the current sidebar directory, with option to include audio only, video only, all media, all non-media, all items, directories or files only.</dd></dl>
  600. <dl class="border_top_x padding_top_1rem"><dt><strong>9. OPEN FONT FILE</strong></dt> <dd>Load a local font file to view information about the font and its complete glyph repertoire.</dd>
  601. <dd>Glyph grids can be navigated with the arrow keys. Individual glyphs can be selected by clicking them or pressing <strong>Return<//strong>.</dd><dd>Individual glyphs may be saved as .svg files.</dd></dl>
  602. <dl class="border_top_x padding_top_1rem"><dt><strong>10. SCRIPT HOME</strong></dt> <dd><strong><a href="https://openuserjs.org/scripts/gaspar_schot/Supercharged_Local_Directory_File_Browser" class="link text_color_default" target="_blank">openuserjs.org</a></strong>.</dd>
  603. <dl><dt><strong>11. HELP</strong></dt><dd>Show this help page.</dd></dl>
  604. <dl><dt><strong>12. BUY ME A COFFEE</strong></dt><dd><strong><a id="donate_link" class="ignore_warning" href="https://www.buymeacoffee.com/fiLtliTFxQ" target="_blank" rel="noopener">Coding is a lot of work...</a></strong></dd></dl>
  605. <dl><dt><strong>13. CONTACT</strong></dt><dd><strong><a id="contact_link" class="ignore_warning" href="mailto:mshroud@protonmail.com">Email</a></strong> me about anything to do with the script.</dd></dl>
  606. </div>
  607. <h2 id="other_functions" class="border_top_x padding_top_1rem"><strong>IV. OTHER SCRIPT FUNCTIONS</strong></h2>
  608. <dl><dt>NAVIGATION</dt>
  609. <dd>Use the up and down arrow keys to navigate items in the sidebar and previewed directories in the content pane.</dd>
  610. <dd>Use the left and right arrow keys to navigate items of the same kind as the currently selected item.</dd>
  611. <dd>Use the Tab key to toggle the focus between the sidebar and the content pane.</dd>
  612. <dd>Type a letter or letters to navigate the directory by file name.</dd>
  613. <dd>Type <b>&#8984;&darr;</b> to navigate to the selected directory.</dd>
  614. <dd>Click a directory icon in the sidebar or select it and type <b>&#8984;&rarr;</b> to open subdirectory; to close, click the icon again or type <b>&#8984;&larr;</b>.</dd></dl><dl><dt>Images, Fonts, Font Glyphs</dt><dd>Previewed items can be scaled with <b>&#8984;+/&ndash;</b> keys.</dd></dl> <dl><dt>IMAGE AND FONT GRIDS</dt><dd>If a directory contains images and/or font files, the "Show Grid" icon will appear in the sidebar. Click it or type <b>&#8984;G</b> to show a grid of the available items.</dd><dd>Grids can be navigated with the arrow keys, and individual grid items may be viewed by clicking them or pressing <b>Return</b>.</dd><dd>When a grid item is being viewed, the grid can still be navigated with the arrow keys.</dd><dd>Closing a selected grid item will show the grid again.</dd></dl>
  615. <dl><dt>PLAYLISTS AND FILELISTS (m3u)</dt><dd>The script supports basic .m3u playlists containing links to audio or video files. It also has custom support for "filelists," which are standard .m3u files that contain links to <i>any</i> type of file or directory.</dd><dd>Note: if you change the extension of an ordinary .m3u file to .txt, the script will read it normally as an editable text file.<br />Double-clicking the selected file in the sidebar or typing <b>&#8984;&darr;</b> or <b>&#8984;+Return</b> will open the playlist/filelist in the sidebar.<br />NOTE: The text must begin with “#EXTM3U for this work.</dd></dl>
  616. <dl><dt>CUE SHEETS (cue)</dt><dd>When a media file (audio or video) is loaded, the script will look for a .cue file in the same directory with <i>EXACTLY</i> the same name as the media file.</dd><dd>If it finds one, it will load the Track ID, the PERFORMER, the TITLE, and the INDEX (time position) into a menu next to the audio player; there is no support for other commands.</dd><dd>Tracks can be selected by clicking the item, and played or paused by clicking the selected item.</dd><dd>.cue files can also be selected independently in the sidebar and edited and saved (locally). This may be handy for creating "on the fly" bookmarks for a long media track before closing the page.</dd><dd>Note that you can also create and save (locally) a new .cue file by using the Text Editor.</dd><dd>Note (MacOS): If you prefer not to clutter the sidebar with .cue files, you may make them invisible by adding a dot to beginning of the file name; the script will still find them.</dd></dl>
  617. <h2 id="troubleshooting" class="border_top_x padding_top_1rem"><strong>V. TROUBLESHOOTING</strong></h2>
  618. <dl><dt>The script doesnt work with a specific directory.</dt><dd>If you have a javascript blocker installed in your browser (and if you don’t, you should), try disabling some of the site-specific scripts and XHR requests, as they may be interfering with the execution of this script.</dd><dd>Alternately, if you do have a javascript blocker installed, you may need to allow some scripts and XHR requests instead.</dd><dd>If the open directory still does not display correctly, check to see if cookies from the site are blocked.</dd><dd>Try deleting preferences from the Main Menu or removing the query string from the URL in the browser.</dd></dl>
  619. <dl><dt>A specific item in a directory does not display correctly.</dt><dd>Confirm that the file is one that browser is capable of rendering. This script cannot display files that the browser itself cannot display.</dd><dd>If the item is from a playlist (m3u) and links to a remote site (e.g., archive.org), check your javascript blocker and cookies for any that need to be allowed from that site.</dd></dl>
  620. <dl><dt>If you think you have found a bug, please <a class="ignore_warning" href="mailto:mshroud@protonmail.com"><b><i>contact me</i></b></a>.</dt></dl><p>&nbsp;</p></div>
  621. </div></section>
  622. </section>`;
  623. let help_elements = ''; if ( body_id === 'top_body' ) { help_elements = `<aside id="help_container" class="background_grey_85 display_none">${ utilities_help_elements }</aside>`; }
  624. return `<div id="utilities" class="position_absolute display_none position_LR_0 z_index_9999"> <aside id="warnings_container" class="overflow_hidden background_grey_90 hyphens_none z_index_9999">${ utilities_warning_elements }</aside> ${ help_elements } </div>`;
  625. }
  626. // ===> END UI HTML
  627. //==============================//
  628. // ===> STYLES
  629. const background_images = `
  630. .menu_item::before { content:""; width:12px; max-width:12px; min-width:12px; height:9px; margin:2px 0 -2px; background-position:center; background-repeat:no-repeat; display:flex; }
  631. .submenu .menu_item::before { width:24px; max-width:24px; min-width:24px; }
  632. .has_background, .has_background_before::before, .has_background_after::after { background-repeat:no-repeat; background-position:center; background-color:transparent !important; }
  633. .bookmark > a::before { background-image:${ get_SVG_UI_Icon("bookmark") }; }
  634. :is(.sort_by_default #menu_sort_by_default, .sort_by_name #menu_sort_by_name, .sort_by_duration #menu_sort_by_duration, .sort_by_size #menu_sort_by_size, .sort_by_date #menu_sort_by_date, .sort_by_kind #menu_sort_by_kind, .sort_by_ext #menu_sort_by_ext, #menu_theme_container, #toggle_text_editing) .menu_item::before, :is(.sort_by_default #sort_by_default, .sort_by_name #sort_by_name, .sort_by_duration #sort_by_duration, .sort_by_size #sort_by_size, .sort_by_date #sort_by_date, .sort_by_kind #sort_by_kind, .sort_by_ext #sort_by_ext) span::before, .loop_media #loop_media_menu::before, .shuffle_media #shuffle_media_menu::before, .background_color_check_mark::before, .texteditor_view_raw #toggle_texteditor_raw::before, .texteditor_view_styled #toggle_texteditor_preview::before, .texteditor_view_html #toggle_texteditor_html::before, body:not(.text_editing_enable_false) #text_editing_enable::before, .texteditor_view_html #texteditor_view_html::before, .cuesheet_track.selected .cue_track_id::before, .menu_item.checkmark::before { background-image:${ get_SVG_UI_Icon("check_mark") }; }
  635. :is( .show_invisibles_false #show_invisible_items, .alternate_background_false #alternate_background, .show_numbers_false #show_numbers, .use_custom_icons_false #use_custom_icons, .show_image_thumbnails_false #show_image_thumbnails, .show_image_thumbnails_always_false #show_image_thumbnails_always, .show_large_image_thumbnails_false #show_large_image_thumbnails, .show_ignored_items_false #show_ignored_items, .ignore_ignored_items_false #ignore_ignored_items, .autoload_index_files_false #autoload_index_files, .media_autoload_false #media_autoload, .media_autoplay_false #media_autoplay, .media_play_all_false #media_play_all, .texteditor_split_view_false:not(.has_texteditor) #texteditor_split_view ) .menu_item.checkmark::before
  636. { background-image:none; }
  637. .sort_by_default #sort_by_default span::after, .sort_by_name #sort_by_name span::after, .sort_by_duration #sort_by_duration span::after, .sort_by_size #sort_by_size span::after, .sort_by_date #sort_by_date span::after, .sort_by_kind #sort_by_kind span::after, .sort_by_ext #sort_by_ext span::after { background-image:${ get_SVG_UI_Icon("chevron") }; background-size:75%; transform:rotate(180deg); }
  638. .is_error #sidebar_nav, .is_error #current_dir_path span::before { background-image:${ get_SVG_UI_Icon("error") }; }
  639. .is_error #sidebar_nav { background-repeat:no-repeat; background-position:center top 6rem; background-size:6rem;}
  640. .is_error #current_dir_path span::before { float:none; display:inline-flex; margin:0 0 -2px 0; width:24px; }
  641. #content_pane[data-content="has_ignored"] #content_container { background-image:${ get_SVG_UI_File_Icon('file_icon_ignored') }; background-size:28px; }
  642. #content_pane.has_audio[data-content="has_null"]:not([data-loaded="unloaded"]) #content_container, #content_pane.has_audio:not([data-content]) #content_container, .has_audio #content_pane[data-content="has_null"]:not([data-loaded="unloaded"]) #content_container, .has_audio #content_pane:not([data-content])[data-loaded="loaded"] #content_container
  643. { background-image:${ get_SVG_UI_Icon("music") }; }
  644. ${ CSS_UI_Icon_Rules() }
  645. #sidebar_menu_main ul a::before { background-image:${ get_SVG_UI_File_Icon('file_icon_file') }; }
  646. #sidebar_menu_main ul a[href^="file"]::before, #current_dir_path span::before { background-image:${ get_SVG_UI_File_Icon('file_icon_dir') }; margin-bottom:-3px; }
  647. #sidebar_menu_main ul a[href^="http"]::before { background-image:${ get_SVG_UI_File_Icon('file_icon_htm') }; }
  648. body.use_custom_icons_false .dir .has_icon_before_before { background-image:${ get_SVG_UI_File_Icon('file_icon_dir_default') }; background-size:auto 13px; }
  649. body.use_custom_icons_false.show_image_thumbnails_false .file:not(.app) .has_icon_before_before, body.use_custom_icons_false:not(.show_image_thumbnails_false) .file:not(.app) .has_icon_before_before
  650. { background-image:${ get_SVG_UI_File_Icon('file_icon_file_default') }; background-size:auto 13px; }
  651. body:not(.use_custom_icons_false).show_image_thumbnails_false .image .has_icon_before_before { background-image:${ get_SVG_UI_File_Icon('file_icon_image') } }
  652. .has_playlist #current_dir_path span::before { background-image:${get_SVG_UI_File_Icon('file_icon_playlist')}; display:inline-flex; margin:-2px 0 0; width:24px; vertical-align:middle;}
  653. .dirlist_item.dir:not(.has_subdirectory) .has_icon_before_before:hover { background-image:${ get_SVG_UI_Icon('chevron') }; transform:rotate(90deg); filter:invert(1); }
  654. .dirlist_item.dir.has_subdirectory .has_icon_before_before:hover { background-image:${ get_SVG_UI_Icon('chevron') }; transform:rotate(180deg); filter:invert(1); }
  655. .dirlist_item.non_local .name_span span::before { background-image:${ get_SVG_UI_Icon('external_link') }; content:""; width:20px; min-width:20px; height:14px; margin-top:-3px; margin-bottom:-3px; background-position:left center; background-repeat:no-repeat; background-blend-mode:screen; display:inline-block; }
  656. .dirlist_item:is(.dir,.other,.system,.bin,.invisible,.markdown):is(.selected,:hover) a .has_icon_before_before { filter:brightness(var(--brightness_low)); }
  657. .theme_light .dirlist_item.audio:is(.selected,:hover) a .has_icon_before_before { mix-blend-mode: hard-light; filter:brightness(0.75) contrast(2) saturate(2.66); }
  658. .theme_dark .dirlist_item.audio:is(.selected,:hover) a .has_icon_before_before, .dirlist_item.content_loaded.non_local .name_span span::before { filter:brightness(1.33); }
  659. .dirlist_item.selected:is(.archive,.app) a .has_icon_before_before, .dirlist_item:is(.archive,.app):hover a .has_icon_before_before { filter:brightness(var(--brightness_high)) saturate(6); }
  660. .dirlist_item.non_local:is(.selected,.audio_loaded) .name_span span::before, body:not(.no_hover) .dirlist_item.non_local:hover .name_span span::before { filter:brightness(2); }
  661. .dirlist_subdir_loading .has_icon_before_before { background-image:${ get_SVG_UI_Icon('spinner') } !important; filter:invert(1); background-size:20px; }
  662. `;
  663. const global_styles = `
  664. .theme_light { --percent_100:100%; --percent_95:95%; --percent_90:90%; --percent_85:85%; --percent_80:80%; --percent_75:75%; --percent_70:70%; --percent_65:65%; --percent_60:60%; --percent_55:55%;
  665. --percent_50:50%; --percent_45:45%; --percent_40:40%; --percent_35:35%; --percent_30:30%; --percent_25:25%; --percent_20:20%; --percent_15:15%; --percent_10:10%; --percent_05:05%; --percent_00:00%;
  666. --border_lum:40%; --border_lum_inverted:40%; --brightness_low:1.15; --brightness_med:1.33; --brightness_high:1.875;
  667. --non_media_item_background_h:216deg; --non_media_item_background_s:100%; --non_media_item_background_l:50%; --non_media_item_background_a:0.8;
  668. --media_item_background_h:180deg; --media_item_background_s:100%; --media_item_background_l:33%; --media_item_background_a:1;
  669. --texteditor_item_background_h:250deg; --texteditor_item_background_s:66%; --texteditor_item_background_l:66%; --texteditor_item_background_a:1.00; }
  670. .theme_dark { --percent_100:00%; --percent_95:05%; --percent_90:10%; --percent_85:15%; --percent_80:20%; --percent_75:25%; --percent_70:30%; --percent_65:35%; --percent_60:40%; --percent_55:45%;
  671. --percent_50:50%; --percent_45:55%; --percent_40:60%; --percent_35:65%; --percent_30:70%; --percent_25:75%; --percent_20:80%; --percent_15:85%; --percent_10:90%; --percent_05:95%; --percent_00:100%;
  672. --border_lum:05%; --border_lum_inverted:40%; --brightness_low:1.15; --brightness_med:1.5; --brightness_high:1.66;
  673. --non_media_item_background_h:216deg; --non_media_item_background_s:80%; --non_media_item_background_l:60%; --non_media_item_background_a:0.8;
  674. --media_item_background_h:180deg; --media_item_background_s:50%; --media_item_background_l:40%; --media_item_background_a:1;
  675. --texteditor_item_background_h:250deg; --texteditor_item_background_s:50%; --texteditor_item_background_l:60%; --texteditor_item_background_a:1.00; }
  676. li, div { --non_media_background: hsla(var(--non_media_item_background_h), var(--non_media_item_background_s), var(--non_media_item_background_l), var(--non_media_item_background_a));
  677. --media_background: hsla(var(--media_item_background_h), var(--media_item_background_s), var(--media_item_background_l), var(--media_item_background_a));
  678. --texteditor_item_background: hsla(var(--texteditor_item_background_h), var(--texteditor_item_background_s), var(--texteditor_item_background_l), var(--texteditor_item_background_a)); }
  679. :root { --font_size_small:0.875rem; color-scheme:none; }
  680. :root, html, body { margin:0; padding:0; border:0; border-radius:0; overflow:hidden; display:flex; flex-direction:row; width:100%; height:100vh; font-family:${UI_Prefs_Non_Bool.ui_font}; font-size:${ UI_Prefs_Non_Bool.ui_font_size}; hyphens:auto; transform-origin:0 0; }
  681. a, a:hover { color:inherit; font-weight:inherit; text-decoration:none !important; }
  682. ul, li { list-style:none; }
  683. svg { margin:auto; }
  684. button, .warning_button { background-color:hsl(0,0%,95%); border:solid 1px #333; border-radius:3px; height:18px; font-size:0.875em; font-family:${UI_Prefs_Non_Bool.ui_font} !important; cursor:pointer; }
  685. button.focus, button:focus, .warning_button { border-radius:3px !important; border-style:solid !important; border-width:1px !important; border-color:#222 !important; }
  686. .selected, .audio_loaded { --background_opacity:1; } :hover, .hovered { --background_opacity:0.75; }
  687. .focus_content #sidebar .selected, .focus_content #sidebar .audio_loaded { --background_opacity:0.50; opacity:1; }
  688. .focus_content #sidebar :hover, .focus_content #sidebar .hovered { --background_opacity:0.25; }
  689. .align_left { text-align:left; } .align_center { text-align:center; } .align_right { text-align:right; } .align_justify { text-align:justify; text-justify:inter-character; hyphens:auto; }
  690. .background_grey_60 { background-color:hsl(0,0%,var(--percent_60)); } .background_grey_65 { background-color:hsl(0,0%,var(--percent_65)); }
  691. .background_grey_70 { background-color:hsl(0,0%,var(--percent_70)); } .background_grey_75, body { background-color:hsl(0,0%,var(--percent_75)); }
  692. .background_grey_80 { background-color:hsl(0,0%,var(--percent_80)); }
  693. .background_grey_85, .dirlist_item:nth-of-type(odd), .cuesheet_track:not(.header):nth-of-type(odd), #utilities_help li:nth-of-type(even), body.alternate_background_false .dirlist_item:nth-of-type(even)
  694. { background-color:hsl(0,0%,var(--percent_85)); }
  695. .background_grey_90, .dirlist_item:nth-of-type(even), .cuesheet_track:not(.header):nth-of-type(even), #stats li:nth-of-type(even) { background-color:hsl(0,0%,var(--percent_90)); }
  696. .background_grey_95, .background_grey_90:not(.no_hover):hover, .background_grey_90.hovered, .background_grey_90.selected, .background_grey_90:focus { background-color:hsl(0,0%,var(--percent_95)); }
  697. .background_grey_100, .background_grey_95:not(.no_hover):hover, .background_grey_95.hovered, .background_grey_95.selected { background-color:hsl(0,0%,var(--percent_100)); }
  698. ${ background_images }
  699. .border_0 { border: none; } .border_all { border: solid 1px hsl(0,0%,var(--border_lum)); }
  700. .border_top { border-top: solid 1px hsl(0,0%,var(--border_lum)); } .border_right { border-right: solid 1px hsl(0,0%,var(--border_lum)); }
  701. .border_bottom { border-bottom: solid 1px hsl(0,0%,var(--border_lum)); } .border_left { border-left: solid 1px hsl(0,0%,var(--border_lum)); }
  702. .border_top_x { border-top: solid 1px hsl(0,0%,var(--border_lum_inverted)); } .border_right_x { border-right: solid 1px hsl(0,0%,var(--border_lum_inverted)); } /* "x" = inverted for theme_dark */
  703. .border_bottom_x { border-bottom: solid 1px hsl(0,0%,var(--border_lum_inverted)); } .border_left_x { border-left: solid 1px hsl(0,0%,var(--border_lum_inverted)); }
  704. .box_shadow_menu { box-shadow:0px 4px 6px -3px #000; }
  705. .display_grid, .info_list:hover li, .has_flyout_menu:hover ul { display:grid; }
  706. .display_none, .error_display_none { display:none; } .display_block { display:block; } .display_flex { display:flex; } .display_inline_flex { display:inline-flex; } .flex_column { flex-direction:column; } flex-direction:column; } .flex_row { flex-direction:row; }
  707. .flex_justify_center { display:flex; flex-direction:column; justify-content:center; flex-grow:1; align-items:center; align-self:stretch; text-align:center; }
  708. .flex_justify_center_row { display:flex; flex-direction:row; justify-content:center; flex-grow:1; align-items:center; }
  709. .flex_justify_contents { justify-content:space-between; }
  710. .flex_grow_1 { flex-grow:1; }
  711. .font_size_small { font-size:var(--font_size_small); }
  712. .has_flyout_menu { outline:none; justify-content:center; align-content:center; }
  713. .has_flyout_menu ul li { width:100%; margin:0; padding:4px 6px; text-align:right; box-sizing:border-box; white-space:pre; grid-column:1; }
  714. .has_flyout_menu ul div { grid-column:2; padding-top:8px; }
  715. .has_flyout_menu .item_1 { grid-row:1; } .has_flyout_menu .item_2 { grid-row:2; }
  716. .height_100 { height:100%; }
  717. .hyphens_none { hyphens:none; }
  718. .info_list { color:hsl(0,0%,var(--percent_10)); }
  719. .info_list li { grid-template-columns:minmax(33%,100%) min(66%); border-top:solid 1px hsl(0,0%,var(--border_lum)); }
  720. .info_list li.info_list_header { border-top:none; }
  721. .info_list li .col_1 { font-weight:bold; text-align:right; border-right:solid 1px hsl(0,0%,var(--border_lum)); }
  722. .info_list span { display:inline-block; padding:4px 6px; }
  723. .line_height_1 { line-height:1; } .line_height_1_2, .info_list span { line-height:1.2; } .line_height_1_4 { line-height:1.4; }
  724. .margin_0, header, footer, nav, ol, ul, li { margin:0; }
  725. .media.local input { cursor:not-allowed; }
  726. .normal { font-weight:normal; }
  727. .outline_none, .outline_none:focus, .outline_none:focus-visible { outline:none; }
  728. .overflow_auto { overflow:auto; } .overflow_hidden { overflow:hidden; } .overflow_visible { overflow:visible; } .overflow_x_hidden { overflow-x:hidden; }
  729. .padding_0, header, footer, nav, a, ol, ul, li { padding:0; }
  730. .padding_4_6 { padding:4px 6px; } .padding_4_8 { padding:4px 8px; } .padding_6_8 { padding:6px 8px; } .padding_top_1rem { padding-top:1rem; } .padding_bottom_1rem { padding-bottom:1rem; }
  731. .pointer, label, input { cursor:pointer; } .cursor_default { cursor:default; }
  732. div:has(> input[disabled]), input[disabled], input[disabled] + label { cursor:not-allowed; }
  733. .position_absolute { position:absolute; } .position_relative { position:relative; } .position_fixed { position:fixed; } .position_0 { top:0; right:0; bottom:0; left:0; } .position_LR_0 { left:0; right:0; }
  734. .resize_none { resize:none; }
  735. .theme_dark .invert { filter:invert(1); }
  736. .transform_rotate_90, .transform_rotate_90_contents > * { transform:rotate(90deg); } .transform_rotate_180, .transform_rotate_180_contents > * { transform:rotate(180deg); }
  737. .transform_rotate_270, .transform_rotate_270_contents > * { transform:rotate(270deg); }
  738. .user_select_none { -webkit-user-select:none; -moz-user-select:none; user-select:none; }
  739. .whitespace_pre { white-space:pre; }
  740. .width_10px, .width_10px_contents > * { width:10px; max-width:10px; min-width:10px; } .width_12px, .width_12px_contents > * { width:12px; max-width:12px; min-width:12px; }
  741. .width_14px, .width_14px_contents > * { width:14px; max-width:14px; min-width:14px; } .width_16px, .width_16px_contents > * { width:16px; max-width:16px; min-width:16px; }
  742. .width_18px, .width_18px_contents > * { width:18px; max-width:18px; min-width:18px; } .width_24px, .width_24px_contents > * { width:24px; max-width:24px; min-width:24px; }
  743. .width_100 { width:100% !important; }
  744. .z_index_1 { z-index:1; } .z_index_2 { z-index:2; } .z_index_3 { z-index:3; } .z_index_9997 { z-index:9997; } .z_index_9998 { z-index:9998; } .z_index_9999 { z-index:9999; }
  745.  
  746. /* NON-MEDIA ITEMS BACKGROUND */
  747. li.selected:not(.media), li:not(.media):not(.no_highlight):hover, li.hovered, li.content_loaded
  748. { background-color:var(--non_media_background) !important; } /* all selected items, hovered non-dirlist items */
  749. li.grid_item:hover, li.grid_item.hovered { --non_media_item_background_a:0.40; background-color:var(--non_media_background); } /* hovered grid items */
  750. #sidebar_nav li:not(.media):hover { --non_media_item_background_a:0.5; background-color:var(--non_media_background); } /* hovered dirlist items */
  751. li.grid_item.selected { --non_media_item_background_a:0.75; background-color:var(--non_media_background); } /* selected grid items */
  752. body.no_hover li.grid_item:is(:not(.selected):hover,.hovered), body.no_hover li.grid_item:is(:not(.selected):hover,.hovered) * { background-color:transparent !important; color:initial !important; }
  753. li.selected + li.selected:nth-of-type(even), li.dir.hovered + li.hovered:nth-of-type(even), li.file.hovered + li.hovered:nth-of-type(odd), .info_list li:nth-of-type(even) { --non_media_item_background_a:0.60; } /* alternate highlight background with multiple selections, show stats*/
  754. /* MEDIA ITEMS BACKGROUND */
  755. li.media[class*="loaded"] { --media_item_background_a:0.9; background-color:var(--media_background) !important; } /* loaded audio, selected video */
  756. li.media.selected:not([class*="loaded"]) { --media_item_background_a:0.7; background-color:var(--media_background) !important; } /* selected audio */
  757. li.media:hover { --media_item_background_a:0.5; background-color:var(--media_background) !important; } /* hovered media */
  758. /* UNHIGHLIGHTED ITEMS: menu visible, .focus_content, .no_hover */
  759. body[class*="has_menu"] #sidebar_nav, body.focus_content #sidebar_nav { --non_media_item_background_s:0%; --media_item_background_s:0%; }
  760. body.theme_light[class*="has_menu"] #sidebar_nav, body.focus_content #sidebar_nav { --media_item_background_l:50%; --non_media_item_background_l:50%; }
  761. body.theme_dark[class*="has_menu"] #sidebar_nav, body.focus_content #sidebar_nav { --media_item_background_l:40%; --non_media_item_background_l:30%; }
  762. body.no_hover #sidebar_menus li:not(.selected):not(.hovered):hover { background-color:inherit !important; color:unset !important; }
  763. body.no_hover #sidebar_nav li:nth-of-type(even):not(.selected):hover { background-color:hsl(0,0%,var(--percent_80)) !important; }
  764. body.no_hover #sidebar_nav li:nth-of-type(odd):not(.selected):hover { background-color:hsl(0,0%,var(--percent_85)) !important; }
  765. body.no_hover #sidebar_nav li:not(.selected):hover { color:unset !important; }
  766. /* TEXTEDITOR ITEMS*/
  767. body:is(.has_texteditor,.texteditor_edited) #show_texteditor li { background-color:var(--texteditor_item_background); }
  768. #show_texteditor li:hover { --texteditor_item_background_a:0.7; background-color:var(--texteditor_item_background) !important; }
  769.  
  770. /* TEXT COLOR */
  771. li:where(.selected,:hover,.hovered),
  772. li:where(.hovered,:hover) li:is(.selected:hover,.selected),
  773. .content_loaded, .audio_loaded, body.has_texteditor #show_texteditor, body.texteditor_edited #show_texteditor,
  774. .no_hover .grid_item:is(.selected,:hover,.hovered), .no_hover .grid_item:is(.selected,:hover,.hovered) *, .grid_item:is(.selected,:hover,.hovered), .grid_item:is(.selected,:hover,.hovered) *, .grid_item.selected::before, .grid_item.selected::after
  775. { color:white !important; } /* white */
  776.  
  777. .text_color_default,
  778. body.no_hover.theme_dark li.grid_item:is(:not(.selected):hover,.hovered), body.no_hover.theme_dark li.grid_item:is(:not(.selected):hover,.hovered) *,
  779. li:is(.selected,:hover,.hovered) li:not(.selected), body.no_hover #sidebar_menus li:is(.selected,:hoever,.hovered) li:not(.selected):hover,
  780. .no_highlight,
  781. .no_highlight:hover,
  782. .no_highlight > li:hover,
  783. :hover:not(#svg_container) > svg,
  784. .font_glyph_item::before,.font_glyph_item::after,#font_specimen_viewer::before,.has_font_specimen_glyph #font_specimen_viewer::after,#font_file_glyph_viewer::before,#font_file_glyph_viewer::after
  785. { color:hsl(0,0%,var(--percent_10)) !important; } /* default */
  786.  
  787. /* FONT WEIGHT */
  788. li:is(.selected,:hover,.hovered) li, body.no_hover li:hover, li:not(.grid_item).no_hover:hover, body.no_hover #sidebar_menus li.hovered li:not(.selected):hover, li:is(.selected,:hover,.hovered) li:not(.selected), body.no_hover #sidebar_menus li:is(.selected,:hoever,.hovered) li:not(.selected):hover { font-weight:normal !important; }
  789. .bold, li:not(.grid_item):hover,li.hovered,li.hovered li:is(:hover,.selected),li:not(.grid_item):not(.audio).selected,li.selected li:hover, li.media[class*="_loaded"], #show_image_grid:hover, #show_font_grid:hover,dt
  790. { font-weight:bold !important; }
  791. #content_pane[data-content="has_ignored"]::before { opacity:0.3; }
  792. .has_warning #sidebar, .has_warning #content_pane, .focus_content .dirlist_item, body:is(.has_menu,.has_menu_parents,.has_menu_stats,.has_menu_grid.has_images.has_fonts) .dirlist_item:not(.hovered), body:is(.has_menu,.has_menu_parents,.has_menu_stats,.has_menu_grid.has_images.has_fonts) #content_pane, #sidebar_menu_parent:not(:hover), #sidebar_menu_main_container:not(:hover) nav, #show_grid_btn, .split_btn span, .disabled:not(.local) { opacity:0.75; }
  793. body.has_menu_footer .dirlist_item.hovered, .disabled:not(.local).selected { opacity:0.84; }
  794. #show_grid_btn:hover, #prev_next_btns span:hover, #sidebar_footer_utilities:hover, .split_btn span:hover { opacity:1.0; }
  795. `;
  796. const utilities_styles = `
  797. #utilities { top:0; justify-content:center; }
  798. #warnings_container { width:26em; flex-direction:column; border-radius:0 0 3px 3px; box-shadow:0px 2px 12px #333; font-size:0.875em; color:#111; display:none; }
  799. #warnings_header { padding:1rem 1.5rem; background-position:left 1.25rem center; background-repeat:no-repeat; background-size:24px; }
  800. #warnings_container:not(.warning_make_playlist) #warnings_header { background-image:${ get_SVG_UI_Icon("error") }; }
  801. #warnings_header h3 { text-indent:2.25em; }
  802. #warnings_container:not(.warning_make_playlist) h3#warning_header, #warnings_container.warning_make_playlist h3#make_playlist_header, .warning_button.show, #warnings div.show, .has_warning #utilities, .has_warning #warnings_container, .has_help #utilities, .has_help #help_container { display:flex;}
  803. #warnings .warning { padding:0 1.5rem 1rem; display:none; }
  804. #warning_buttons_container { padding:1rem 1.5rem; }
  805. button.focus, button:focus { background-color:#0E4399; color:#EEE; outline:none; }
  806. .warning_button { min-width:4em; display:none; font-size:1em; justify-content:center; }
  807. #warning_btn_cancel + #warning_btn_save, #warning_btn_dont_save + #warning_btn_cancel, #warning_btn_clear { margin-right:auto; }
  808. #warning_btn_cancel, #warning_btn_clear, #warning_btn_save { margin-left:0.5rem; }
  809. #warnings_container.clear #warning_buttons { justify-content:space-between; }
  810. #warning_make_playlist fieldset div { padding:0 0 2px; }
  811. #warning_make_playlist .indent { text-indent:2em; }
  812. #warning_make_playlist input { margin-right:6px; }
  813. .has_warning::before, .has_overlay::before { content:""; position:fixed; top:0; right:0; bottom:0; left:0; z-index:9998;-webkit-user-select:none;-moz-user-select:none; user-select:none; }
  814.  
  815. #help_container { padding:0 1em 1em; overflow:auto; } #help_container > header { grid-template-columns:5em auto fit-content(100%); } #help_container > section { padding-top:2rem; }
  816. #help_container dd { margin-inline-start:1em; } #help_container dd:before { content:"\u2219"; margin-right:6px; }
  817. #help_container dl + p { margin-top:1rem; padding-top:1rem; } #help_container ol li { list-style:decimal; }
  818. #help_container kbd { min-width:1em; height:fit-content; padding:2px 6px; display:inline-block; border:solid 1px #888; border-radius:3px; text-align:center; font-family:inherit; font-size:0.875em; background-color:hsl(0,0%,var(--percent_90)); } #help_main_menu svg { margin: 0 0 -2px; width:12px; }
  819. #help_bookmarks::before { background-image: ${ get_SVG_UI_Icon("bookmark") }; }
  820. .has_help #utilities, .has_help #help_container { bottom:0; }
  821. `;
  822. const sidebar_header_menu_styles = `
  823. /* PARENTS MENU */
  824. #parent_dir_nav #svg_chevron { width:18px; } #parent_dir_nav #svg_multiply { width:14px; }
  825. #parent_dir_nav #svg_multiply, body:is(.has_playlist,.has_filelist) #svg_chevron, body:is(:not(.has_playlist),:not(.has_filelist)) #sidebar_header #close_playlist_container { display:none; }
  826. body.has_playlist #parent_dir_nav #svg_multiply, body.has_filelist #parent_dir_nav #svg_multiply, body:is(.has_playlist,.has_filelist) #sidebar_header #close_playlist_container { display:flex; }
  827. #current_dir_path { padding:3px 6px; word-break:break-word; }
  828. /* MAIN MENU */
  829. #sidebar_menu_main li { display:flex; }
  830. .has_menu #sidebar_menu_main, .has_menu_parents #parents_links, body:not(.no_hover) #sidebar_menus .has_submenu:hover .submenu, #sidebar_menu_main .has_submenu.hovered .submenu, #sidebar_menu_main li.has_submenu.selected .submenu { display:block; }
  831. #sidebar_menu_main li.has_submenu { position:relative; justify-content:space-between; }
  832. #sidebar_menu_main li.bookmark a::before { content:""; width:24px; max-width:24px; min-width:24px; height:12px; background-size:12px; }
  833. .submenu { width:100%; max-width:240px; display:none; margin:0; padding:0; box-sizing:border-box; position:absolute; left:100%; }
  834. #sidebar_menu_main ul.submenu li a { padding:6px 8px 6px 0; }
  835. #sidebar_menu_main input { width:0; float:left; }
  836. .menu_item { margin:0; padding:5px 8px 5px 0; display:flex; flex-grow:1; text-align:left; }
  837. #sidebar_menu_main .selected ~ li:hover .submenu, #sidebar_menu_main .selected ~ li .submenu:hover, .has_open, #sidebar_menu_main .show_input span.menu_item, .text_editing_enable_false:not(.has_texteditor) #sidebar_menu_main li#texteditor_split_view, .text_editing_enable_false:not(.has_texteditor) #toggle_texteditor_html_menu { display:none; }
  838. #sidebar_menu_main .show_input input { display:unset; margin:2px 6px; width:100%; }
  839. /* menu right arrow */
  840. #sidebar_menu_main svg { margin: 0 6px; width:12px; }
  841. #sidebar_menu_main li:is(.selected,.hovered:not(:hover),:hover) svg { filter:invert(1); } body.no_hover:not(.theme_dark) #sidebar_menu_main li:not(.selected):not(.hovered):hover svg { filter:invert(0); }
  842. #ui_scale span.menu_item::after { content:attr(data-value); } #ui_scale_input_container, #ui_scale.show_input span + span { display:flex; } #ui_scale_input_container { padding-left:18px; padding-right:8px; }
  843. /* IFRAME MENUS */
  844. #parent { padding:5px 3px 5px 0; }
  845. #parent span { padding:0px 1px; }
  846. #open_in_sidebar { padding:5px 2px 5px 3px; }
  847. `;
  848. const sidebar_header_styles = ` /* for both sidebar and content_iframe */
  849. #sidebar { font-variant-numeric:tabular-nums; } #sidebar.top_body { min-width:200px; } #sidebar.iframe { min-width:500px; flex-basis:100%; }
  850. #sidebar_header_title_div { letter-spacing:0.5em; text-indent:0.75em; flex-basis:100%; }
  851. #sidebar_header_title_div:before { content:"INDEX OF"; } .has_playlist #sidebar_header_title_div:before { content:"PLAYLIST"; } .has_filelist #sidebar_header_title_div:before { content:"FILELIST"; }
  852. ${ sidebar_header_menu_styles }
  853. /* SIDEBAR BUTTONS */
  854. #directory_buttons_left { padding:6px; }
  855. #show_details { margin-top:0; margin-right:8px; padding:0 4px; }
  856. #show::before { content:"Show "; }
  857. /* GRID BTN ---> combine style with save_btn */
  858. #show_grid_btn { margin:0 0 0 auto; }
  859. #show_grid_btn ul { padding-left:0px; top:-1px; right:-1px; }
  860. #show_grid_btn ul:after { content:""; position:absolute; z-index:-1; top:0; bottom:0; left:0; right:0; background-color:hsl(0,0%,var(--percent_80)); }
  861. #show_grid_btn ul:not(:has(li:hover)):hover { background-color:var(--non_media_background); }
  862. #show_grid_btn ul:not(:has(li:hover)):hover svg { color:white !important; }
  863. #show_grid_btn.has_grid div { color:#118888; }
  864. #top_body:is(.has_images,.has_fonts) #show_grid_btn { display:flex; }
  865. #top_body.has_images.has_fonts #show_grid_btn:hover ul { display:grid; }
  866. /* SORTING ITEMS */
  867. #sidebar_header_utilities_row_2 .sorting { grid-row:1; }
  868. body:not(.show_details_false) #sorting_row_2 { display:grid; }
  869. #sorting_row_1 span, #sorting_row_2 span, .sorting span::before, .sorting span::after { display:inline-block; }
  870. #sorting_row_1 span, #sorting_row_2 span { padding:6px 0; }
  871. #sorting_row_1.iframe span, #sorting_row_2.iframe span { padding:4px 0; }
  872. #sidebar_header_utilities_row_2 span::before, #sidebar_header_utilities_row_2 span::after, .sorting .menu_item::after { content:""; width:16px; height:8px; color:#CCC; background-position:center; background-repeat:no-repeat; background-size:10px; }
  873. .sorting.down span::after, .sort_direction_descending .sorting span::after { transform:rotate(0deg) !important; }
  874. #sort_by_name input { margin:-2px 6px -2px 0; bottom:-2px; }
  875. #sort_by_ext { grid-column: span 2; }
  876. .has_media #sort_by_ext { grid-column: span 1; }
  877. .has_media #sort_by_default { text-align:center; }
  878. #sort_by_default.iframe, .iframe #sort_by_size, .iframe #sort_by_date { text-align:right; }
  879. .has_media #sort_by_duration, .has_playlist #sort_by_duration, #content_body.has_media #sorting_row_2 #sort_by_duration { display:unset; }
  880. #content_body #sorting_row_1 #sort_by_duration, #top_body #sorting_row_2 #sort_by_duration { display:none; }
  881. /* TEXT EDITOR ITEM */
  882. body:not(.show_details_false) #show_texteditor, body.has_texteditor #show_texteditor { display:flex; }
  883. #show_texteditor a { padding-left:10px; }
  884. `;
  885. const sidebar_nav_styles = ` /* for both sidebar and content dirlists */
  886. #sidebar_nav { overflow-y:hidden; flex-basis:100%; }
  887. #dir_nav_inner { overflow:auto; margin-bottom:-1px; }
  888. #sidebar_nav ol { -webkit-margin-before:0em !important; -webkit-margin-after:0em !important; -webkit-padding-start:0em; }
  889. #directory_list { counter-reset:item; transition:opacity .125s; }
  890. #directory_list:empty { border-bottom:0; padding:100%; }
  891. .dirlist_item_input, .dirlist_item_details, .dirlist_item_details span, .dirlist_item_media_duration, .details.ext, .dirlist_item.error::before, .dirlist_item_name_a::before,
  892. body.show_invisibles_false:not(.has_menu_stats) .dirlist_item.invisible.ignored, body.show_invisibles_false:not(.show_ignored_false):not(.has_menu_stats) .dirlist_item.invisible:not(.ignored),
  893. body.show_invisibles_false.show_ignored_items_false:not(.has_menu_stats) .dirlist_item.ignored, body.show_invisibles_false.show_ignored_items_false:not(.has_menu_stats) .dirlist_item.invisible,
  894. body.show_ignored_items_false:not(.show_invisibles_false):not(.has_menu_stats) .dirlist_item.ignored:not(.invisible) { display:none; }
  895. .dirlist_item { margin-inline-start:0; display:grid; grid-gap:0; }
  896. .top_item { grid-template-columns:minmax(8rem,auto) minmax(6em,1fr) minmax(auto,6em); }
  897. .dirlist_item_name { grid-row:1; display:flex; -webkit-padding-start:0; -moz-padding-start:0; word-break:break-word; }
  898. .top_item .dirlist_item_name { grid-column:1 / span 3; padding:6px 12px 6px 0; }
  899. .dirlist_item_name::before { counter-increment:item; content:counter(item); min-width:36px; height:14px; max-height:14px; min-height:14px; text-align:right; padding:0 3px 0 0; text-indent:6px; }
  900. .dirlist_item_input { margin:1px 6px 0 0; max-height:13px; }
  901. .dirlist_item .desc.dirlist_item_details { padding:0 6px 4px 40px; grid-column:1 / span 3; text-align:left; white-space:unset; }
  902. .has_icon_before::before, .has_icon_before_before, .has_icon_after::after { content:""; display:inline-block; background-position:center; background-repeat:no-repeat; background-size:14px,0px; }
  903. .has_icon_before::before, .has_icon_before_before, .show_large_image_thumbnails_false .dirlist_item.image .has_icon_before_before, .show_image_thumbnails_false .dirlist_item.image .has_icon_before_before, .has_icon_after:not([data-after])::after { width:14px; height:14px; max-width:20px; max-height:14px; min-width:14px; min-height:14px; margin:0 6px; }
  904. .dirlist_item.image .has_icon_before_before { width:56px; height:56px; max-width:56px; max-height:56px; min-width:56px; min-height:56px; margin:0 6px; background-position:top; background-size:contain,0px; }
  905. .ignored:not(.selected) .has_icon_before::before, .ignored:not(.selected) .has_icon_before_before, .focus_content .has_icon_before_before, .has_quicklook #sidebar { filter:grayscale(100%); }
  906. .focus_content li:is(.selected,.hovered,:hover) .has_icon_before_before { filter:grayscale(0%); }
  907. /* MEDIA ITEMS */
  908. .top_item.media .dirlist_item_name { grid-column:1 / span 2; padding-right:0; }
  909. .top_item.media .dirlist_item_media_duration { grid-column:3; padding:6px 12px 6px 0; }
  910. .dirlist_item.media .dirlist_item_media_duration { grid-row:1; }
  911. .iframe_item.media .dirlist_item_media_duration { grid-column:2; }
  912. .media:not(.local) .dirlist_item_media_duration { display:unset; }
  913. .dirlist_item.media .dirlist_item_media_duration:not([data-duration="NaN"]):empty
  914. { background-image:${ get_SVG_UI_Icon('spinner') } !important; background-position:top 3px right 10px; background-repeat:no-repeat; background-size:20px; }
  915. .dirlist_item_media_duration[data-duration="NaN"]::after { content:"[Error]"; }
  916. .theme_dark .dirlist_item_media_duration:empty { filter:invert(1); }
  917. /* SORTING BORDERS */
  918. .sort_by_default:not(.show_invisibles_false) .dir.invisible + .dir:not(.invisible), .sort_by_default:not(.show_invisibles_false) .dir:not(.invisible) + .dir.invisible { border-top:solid 1px hsl(0,0%,var(--border_lum)); }
  919. /* ITEM DETAILS */
  920. .dirlist_item_details { text-align:right; white-space:nowrap; }
  921. .top_item .dirlist_item_details { padding:0 12px 4px 0; }
  922. .dirlist_item_details.size { padding-left:12px; }
  923. .dirlist_item_details.date { padding-bottom:0; height:1em; max-height:1em; overflow-wrap:break-word; }
  924. .dirlist_item_details.kind::first-letter { text-transform:uppercase; }
  925. #content_body .iframe_item { grid-template-columns: minmax(20em,100%) minmax(4em,6em) minmax(6em,8em) minmax(6em,14em) minmax(6em,8em); }
  926. #content_body .iframe_item.non_media .dirlist_item_name { grid-column:1 / span 2; }
  927. #content_body .iframe_item .dirlist_item_details { grid-row:1; height:1ex; }
  928. #content_body .iframe_item .desc.dirlist_item_details { grid-row:2; grid-column:1 / span 6; height:auto; padding:0 6px 4px 40px; }
  929. #content_body .iframe_item .dirlist_item_name_a, #content_body .iframe_item > span { padding:5px 16px 5px 0; }
  930.  
  931. body:not(.show_numbers_false) .dirlist_item_name_a::before { display:initial; }
  932. body:not(.show_details_false) .dirlist_item_details, .media .dirlist_item_input { display:unset; }
  933. .disabled, .ignore_ignored_items li.ignored, .has_filelist [id$="sort_by_size"], .has_playlist [id$="sort_by_size"], .has_filelist [id$="sort_by_date"], .has_playlist [id$="sort_by_date"]
  934. { cursor:not-allowed; opacity:0.75; }
  935. .dirlist_item.error { display:block; padding:6px 8px; }
  936. .dirlist_item.ignored.local .dirlist_item_name_a::after { content:"\\00a0[local file]"; display:contents; font-style:italic; }
  937. .is_error #is_error { display:block !important; grid:none !important; grid-template-columns:none !important; }
  938. .is_error #is_error_items { display:block; }
  939.  
  940. `;
  941. const iframe_dir_styles = `${ global_styles }
  942. #content_body { overflow-x:auto; font-size:${ (parseFloat(UI_Prefs_Non_Bool.ui_font_size) * 0.875) + UI_Prefs_Non_Bool.ui_font_size.replace(/\d*/,'') }; }
  943. .theme_dark .sorting span::before, .theme_dark .sorting span::after { filter:invert(1); }
  944. #content_body:not(.show_details_false) #show::before { content:"Hide "; }
  945. #content_body.show_details_false .iframe_item { grid-template-columns:auto; }
  946. #content_body.show_details_false .media .dirlist_item_media_duration, #content_body #content_pane, #content_body .content_el,
  947. #content_body.has_quicklook div[id^="title_buttons"], #content_body.has_quicklook #content_pane.has_audio :is(#content_title_container,#content_container),
  948. #content_body.has_quicklook #content_pane[data-content="has_ignored"] #content_iframe, #content_body.has_quicklook #content_pane[data-content="has_pdf"] #content_pdf { display:none; }
  949. #content_body.has_quicklook #sidebar_nav { opacity:0.6; }
  950. #content_body.has_quicklook #content_pane { display:flex; padding:2em; position:absolute; z-index:1; left:0; right:0; top:0; bottom:0; justify-content:center; }
  951. #content_body.has_quicklook #content_header { background-color:hsl(0,0%,var(--percent_85)); border-radius: 3px 3px 0 0; border:solid 1px hsl(0,0%,var(--border_lum)); border-bottom:0; }
  952. #content_body.has_quicklook #content_container { padding:6px; background-color:hsl(0,0%,var(--percent_85)); box-shadow:0 0 12px #000; border-radius: 0 0 3px 3px; border:solid 1px hsl(0,0%,var(--border_lum)); border-top:0; contain:unset; flex-basis:unset; }
  953. #content_body.has_quicklook .content_el { width:100%; }
  954. #content_body.has_quicklook .content_el.has_content { border:solid 1px hsl(0,0%,var(--border_lum)); box-sizing:border-box; }
  955. #content_body.has_quicklook .content_el.has_content,#content_body.has_quicklook .content_el:has(.has_content) { display:flex; z-index:1; }
  956. #content_body.has_quicklook #content_container:has(#content_font.has_content, #content_pdf.has_content,#content_iframe.has_content),#content_body.has_quicklook #content_pane[data-content="has_ignored"] #content_container { height:50%; flex-basis:unset; }
  957. #content_body.has_quicklook #content_container:has(#content_image.has_content) { display:table; flex-basis:unset; }
  958. #content_body.has_quicklook #content_video.has_content { position:static; }
  959. #content_body.has_quicklook #content_pane:not([data-content="has_ignored"]) #content_font.has_content { display:grid; }
  960. #content_body.has_quicklook #content_pane[class*="has_font_specimen"] #font_specimen_viewer { display:flex; }
  961. #content_body.has_quicklook #content_image_container, #content_body.has_quicklook #content_iframe { max-height:88vh; }
  962. #content_header, #content_body.has_quicklook #content_image_container, #content_body.has_quicklook #content_container:has(#content_video.has_content) { padding:0; }
  963. #content_pane[data-content="has_pdf"] #content_container { background-image:${ get_SVG_UI_File_Icon('file_icon_ignored') }; background-size:28px; }
  964. `;
  965. const sidebar_footer_styles = ` /* for both sidebar and content_iframe */
  966. .has_menu_stats #stats_summary, .stats_kind span.file, .stats_kind span.media, .has_menu_stats #sidebar_footer_utilities { display:none; }
  967. #stats_container { max-height:33vh; }
  968. .theme_light #sidebar_footer_utilities:hover ul, .theme_light #sidebar_footer:hover, .theme_light #stats_details_summary { box-shadow:0px -4px 4px 0px rgba(128,128,128,0.6); }
  969. .theme_dark #sidebar_footer_utilities:hover ul, .theme_dark #sidebar_footer:hover, .theme_dark #stats_details_summary { box-shadow:0px -4px 4px 0px rgba(32,32,32,0.6); }
  970. #stats_details_summary_dirs .stats_kind::before { background-image:${ get_SVG_UI_File_Icon("file_icon_dir") }; }
  971. #stats_details_summary_files .stats_kind::before { background-image:${ get_SVG_UI_File_Icon("file_icon_file_default") }; }
  972. #stats_details_summary, #stats_details_items_container { overflow-y:scroll; }
  973. #stats_summary_totals, .has_media #total_duration { display:flex; text-align:left; white-space:normal; padding-right:1em; }
  974. #stats a { padding:3px 12px 3px 0; }
  975. #stats a::before { content:attr(data-count); width:36px; text-align:right; }
  976. .stats_kind span { margin-right:0.5em; white-space:pre; display:flex; }
  977. .stats_kind > span::first-letter { text-transform:uppercase; }
  978. #stats_details_items li.audio a span span::after { content:attr(data-audio_duration); white-space:pre; }
  979. #stats_details_items li.video a span span::after { content:attr(data-video_duration); white-space:pre; }
  980. #total_duration::after { content:attr(data-time_remaining); white-space:pre; }
  981. .has_media #total_duration::before { content:"Total Time:\\00a0"; }
  982. .stats_list_item_name_a { -webkit-padding-start:0; padding:1px 0; } .stats_list_item span::first-letter { text-transform:uppercase; }
  983. #stats_summary, #stats_details_summary { margin-block-start:0; margin-block-end:0; }
  984. #stats a.dirlist_item_name_a:before { display:inline-block; }
  985. #stats_details_items { max-height:25vh; }
  986. #stats_container, .has_menu_stats #stats_details_summary, .has_menu_stats #stats_details_items { display:block; }
  987. #sidebar_footer_utilities { right:-1px; } #sidebar_footer_utilities svg { margin:2px 2px 0 0; opacity:0.75; }
  988. #sidebar_footer_utilities ul { bottom:0; right:-1px; white-space:nowrap; box-shadow:-0px -3px 6px -3px #333; } #sidebar_footer_utilities:hover ul { display:block; }
  989. `;
  990. const sidebar_utilities_styles = `
  991. #show_sidebar { top:0; right:0; height:21px; opacity:0.75; } #show_sidebar:hover { opacity:1; }
  992. body.show_sidebar_false #handle { display:none; } body.show_sidebar_false #show_sidebar { left:2px; transform:rotate(180deg); }
  993. body.show_sidebar_false #sidebar { width:0 !important; min-width:0; position:absolute; top:2px; left:-1px; }
  994. body.show_sidebar_false #sidebar_header { z-index:unset; display:none; } body.show_sidebar_false #sidebar_nav { visibility:hidden; }
  995. body.show_sidebar_false #directory_list_outer { min-width:0; } body.show_sidebar_false #content_pane { width:100% !important; }
  996. body.show_sidebar_false #title_buttons_left { padding-left:24px; } #handle { top:0; bottom:0; right:-4px; width:7px; cursor:col-resize; }
  997. .has_overlay #handle { z-index:9999; }
  998. `;
  999. const sidebar_styles = `${ sidebar_header_styles } ${ sidebar_nav_styles } ${ sidebar_footer_styles } ${ sidebar_utilities_styles }`;
  1000. /* CONTENT PANE STYLES */
  1001. const content_pane_header_styles = `
  1002. /***** CONTENT TITLE *****/
  1003. #content_title_container { overflow-x:scroll; }
  1004. #content_title { min-width:16em; min-height:18px; padding:4px 8px; word-break:break-word; } #content_title *:empty { display:none; }
  1005. #content_title span { pointer-events:none; } #content_title span::before, #content_title span::after { font-weight:normal; margin-bottom:-3px; }
  1006. #content_pane.has_font_specimen #content_title div::before { content:"Font"; }
  1007. #content_pane.has_font_file #content_title div::before, #content_pane.has_font_file_glyph #content_title div::before { content:"Glyphs from font"; }
  1008. .has_directory_source #content_title div::before { content:"Source of" !important; }
  1009. .has_directory_source #content_title span::before { background-image:${ get_SVG_UI_File_Icon("file_icon_dir_default") }; height:14px !important; background-size:contain; }
  1010. #content_pane[data-content="has_grid"] #content_title div::before { content:"Fonts and Images from"; }
  1011. #content_pane[data-content="has_grid"].has_font_grid #content_title div::before { content:"Fonts from"; }
  1012. #content_pane[data-content="has_grid"].has_image_grid #content_title div::before { content:"Images from"; }
  1013. #content_pane[data-content="has_ignored"] #content_title div::before { content:"Ignored content"; }
  1014. #content_pane[data-content="has_dir"] #content_title div::before { content:"Index of"; }
  1015. body.has_texteditor.texteditor_view_raw.texteditor_split_view_false #content_title div::after { content:" (Source Text)"; }
  1016. body.has_texteditor.texteditor_view_styled.texteditor_split_view_false #content_title div::after { content:" (Text Preview)"; }
  1017. body.has_texteditor.texteditor_view_html.texteditor_split_view_false #content_title div::after { content:" (HTML Preview)"; }
  1018. body.has_texteditor #content_title div::before { content:"Text Editor" !important ; font-weight:bold; }
  1019. body.texteditor_edited.has_texteditor #content_title div::before { content:"Text Editor (edited)" !important; font-weight:bold; }
  1020. body.has_texteditor #content_title span { display:none; }
  1021. body.has_texteditor #content_title span::before { background-image:${ get_SVG_UI_File_Icon("file_icon_markdown") }; }
  1022. body:not(.text_editing_enable_false) #content_pane:is([data-content="has_text"],[data-content="has_code"],[data-content="has_markdown"]) #content_title span::after
  1023. { background-image:url("data:image/svg+xml;utf8,${ SVG_UI_File_Icons.file_icon_ebook}"); pointer-events:all; opacity:0.66; }
  1024. body.text_editing_enable_false #content_pane:is([data-content="has_text"],[data-content="has_code"],[data-content="has_markdown"]) #content_title span::after
  1025. { background-image:url("data:image/svg+xml;utf8,${ SVG_Text_Editing_UI_Icons.text_editing }"); pointer-events:all; opacity:0.66; }
  1026. #content_pane:is([data-content="has_text"],[data-content="has_code"],[data-content="has_markdown"]) #content_title:hover span::after { opacity:1 !important; }
  1027. body.theme_dark #content_pane:not([data-content="has_image"]):not([data-content="has_grid"]) #content_title span::after { filter:invert(1) !important; }
  1028. #content_pane[data-loaded="unloaded"] #content_title { display:flex; justify-content:center; align-items:center; }
  1029. #content_pane[data-loaded="unloaded"] #content_title div::before { content:"Loading..." }
  1030. #content_pane[data-loaded="unloaded"] #content_title span { display:none; }
  1031. #content_pane[data-content="has_grid"] #content_title span::before { background-image:${ get_SVG_UI_File_Icon("file_icon_dir") }; height:14px !important;}
  1032. #content_pane[data-content="has_grid"] #content_title span::after { content:attr(data-grid_count_items); font-weight:normal; white-space:pre; margin:0; }
  1033. #content_pane[data-content="has_image"] #content_title span::after { content:attr(data-after); font-weight:normal; white-space:pre; margin:0; }
  1034. #content_pane.content_error #content_title span::before, #content_pane.content_error #content_container, #content_pane.has_audio_error #content_audio_title span::before {background-image:${ get_SVG_UI_Icon("error")}; }
  1035. body.is_error:not(.has_directory_source) #content_title span::before, #content_pane.content_error #content_title div::before { content:"ERROR:"; white-space:pre; display:inline; }
  1036. /* CONTENT TITLE BUTTONS LEFT */
  1037. #reload_btn { width:52px; } #reload_btn::before { content:"Reload"; }
  1038. #prev_next_btns { margin-left:4px; } #prev_next_btns span { width:2em; height:16px; } #prev_next_btns span:active { background-color:#0E4399; } #prev_next_btns:focus { background-color:white; }
  1039. #prev_next_btns svg { width:12px; }
  1040. /* CONTENT TITLE BUTTONS RIGHT */
  1041. #scale { margin-right:4px; background-color:#FFF; } #scale span { width:2em; }
  1042. #close_btn { width:52px; } #close_btn::before { content:"Close"; } body.has_texteditor #close_btn::before { content:"Hide"; } #content_pane[data-content="has_null"] #close_btn { display:none !important; }
  1043. .split_btn::after { content:""; position:absolute; top:0; bottom:0; left:calc(50% - 1px); border-left:solid 1px #333; } .split_btn span { display:inline-flex; }
  1044. #open_in_texteditor { margin-right:4px; }
  1045. `;
  1046. const content_pane_audio_styles = `
  1047. /* CONTENT AUDIO TITLE */
  1048. #content_audio_title span { padding:4px 6px 0; }
  1049. #content_audio_title span::before { content:""; padding-right:22px; height:14px !important; font-weight:normal; background-image:${ get_SVG_UI_File_Icon("file_icon_audio") }; background-position:center; background-position:right 4px center; background-repeat:no-repeat; }
  1050. #content_pane.has_audio #content_audio_title span::before, #content_pane[data-content="has_video"] #content_title div::before { content:"Playing:"; }
  1051. #content_pane.has_audio.has_audio_error #content_audio_title span::before { content:"ERROR:"; }
  1052. #content_pane.has_audio.has_audio_error #content_audio { padding-top:0; }
  1053. #content_pane.has_audio.has_audio_error #audio_container { display:none; }
  1054. /* CONTENT AUDIO PLAYER */
  1055. #content_audio { justify-content:center; padding:2px 6px 6px; overflow-x:auto; }
  1056. #audio_container { height:32px; background-color:rgb(241, 243, 244); }
  1057. #prev_track, #next_track { width:2rem; }
  1058. #audio { height:32px; }
  1059. audio::-webkit-media-controls-enclosure { border-radius:0; }
  1060. #close_audio { width:32px; }
  1061. #audio_options { margin-top:0; margin-right:calc(-6em - 8px); padding:0 4px; width:6em; justify-content:start; }
  1062. #loop_label input { margin:0px 4px 2px; }
  1063. #shuffle_label input { margin:2px 4px 0px; } #shuffle_label::after { content:attr(data-shufflecount); }
  1064. /* CUE SHEET MENU */
  1065. .cuesheet_track_list_container { background-image:${ get_SVG_UI_File_Icon("file_icon_playlist") }; background-repeat:no-repeat; background-size:18px; background-color:inherit; display:none; }
  1066. .cuesheet_track_list_container:hover > div { display:flex; flex-direction:column; margin-top:-1px; overflow:auto; }
  1067. .cuesheet_track_list_container:hover .cuesheet_track_list, #content_grid a { display:block; }
  1068. #cuesheet_track_list_container_audio { width:32px; background-position:center; } #cuesheet_track_list_container_video { width:24px; background-position:top left; }
  1069. #cuesheet_track_list_container_audio > div { padding-top:13px; } #cuesheet_track_list_container_video > div { padding-top:10px; }
  1070. .cuesheet_track { justify-content:space-between; grid-template-columns: 2rem minmax(6rem,1fr) minmax(6em,1fr) minmax(auto,6em); }
  1071. .cuesheet_track.selected .cue_track_id::before { content:""; width:16px; height:8px; display:flex; }
  1072. .cuesheet_track span { padding:4px 0 4px 8px; font-variant-numeric:tabular-nums; }
  1073. #cuesheet_title { padding:4px 8px; font-variant-numeric:tabular-nums; text-align:center; }
  1074. /* CONTENT TITLE PLAYLIST ENTRY (#content_playlist_item and #content_audio_playlist_item) */
  1075. .playlist_entry_container { flex-direction:row; }
  1076. .playlist_entry_container textarea { resize:vertical; background-color:transparent; }
  1077. .theme_light .playlist_entry_container:has(textarea:focus) { box-shadow:inset 0px 0px 2px 2px var(--non_media_item_background); }
  1078. .theme_dark .playlist_entry_container:has(textarea:focus) { box-shadow:inset 0px 0px 4px 1px hsl(0,0%,var(--percent_95)); }
  1079. #content_pane.has_audio #audio_wrapper, .playlist_entry_container.has_content { display:flex; flex-direction:column; }
  1080. audio::-webkit-media-controls-panel { padding:0; }
  1081. `;
  1082. const content_pane_styles = `${ content_pane_header_styles } ${ content_pane_audio_styles }
  1083. #content_pane { transform:scale(1); contain:strict; }
  1084. #content_container { align-items:center; justify-content:center; bottom:0; background-position:center; background-repeat:no-repeat; background-size:33.33%; contain:strict; flex-basis:100%; overflow:auto; }
  1085. .content_el { width:100%; height:100%; margin:0; padding:0; overflow:auto; display:none; }
  1086. #content_pane[data-loaded="unloaded"] .content_el { display:none !important; }
  1087. #content_pane[data-loaded="unloaded"]:not([data-content="has_ignored"]) #loading_spinner { display:block; }
  1088. /* CONTENT DISPLAY */
  1089. #content_pane:not([data-content="has_ignored"]) #content_font.has_content, #content_pane[data-content="has_image"] #content_image_container, body:not(.has_texteditor) #content_pane[data-content="has_grid"] #content_grid
  1090. { display:grid; }
  1091. #content_pane[data-content="has_grid"] .split_btn, #content_pane[data-content="has_image"] .split_btn, #content_pane[data-content="has_font"] .split_btn, #content_body:not(.text_editing_enable_false) #content_pane[data-content="has_htm"] #open_in_texteditor, #content_pane:not([data-content="has_ignored"]) .content_el:not(#content_font).has_content, body.has_texteditor #content_texteditor, #content_pane[class*="has_font_specimen"] #font_specimen_viewer, .has_font_specimen_glyph #font_specimen_glyph_viewer, #content_pane[class*="has_font_file"] #font_file_viewer, .has_font_file_glyph #font_file_glyph_viewer
  1092. { display:flex; }
  1093. /* CONTENT GRID (div) */
  1094. #content_grid { font-size:1rem; grid-gap:0; grid-template-columns:repeat(auto-fill, minmax(${ ( UI_Prefs_Non_Bool.grid_image_size + 16) }px, auto)); grid-auto-rows:minmax(min-content, max-content); }
  1095. #content_pane.has_hidden_grid #content_grid { max-height:100%; overflow:hidden; position:absolute; display:grid; margin-left:-100%; }
  1096. /* FONT & IMAGE GRID ITEMS */
  1097. .image_grid_item { padding:6px; grid-column:auto; line-height:0; }
  1098. .image_grid_item img { width:auto; max-width:${ (UI_Prefs_Non_Bool.grid_image_size).toString() }px; max-height:${ (UI_Prefs_Non_Bool.grid_image_size) }px; position:relative; opacity:0.9; }
  1099. .image_grid_item img[src$=".svg"] { width:100%; height:100%; }
  1100. .image_grid_item.selected { box-shadow:inset 0px 0px 4px hsl(0,0%,var(--percent_60)); } .font_grid_item.selected p { font-weight:bold; } .font_grid_item.selected a { font-weight:unset; }
  1101. .image_grid_item.selected img, .font_grid_item.selected, .image_grid_item:hover, .font_grid_item:hover { opacity:1; }
  1102. .font_grid { font-size:4em !important; margin:0 0 20px; grid-gap:0; grid-template-columns:repeat(auto-fit, minmax(max(60px,1.33em), 1.5fr)); grid-auto-rows:minmax(max(60px,1.33em), max-content); line-height:unset !important; letter-spacing:unset !important; }
  1103. .font_grid_item { line-height:1; padding:8px 20px; grid-column:1 / -1; opacity:0.9; }
  1104. .font_grid_item_info { padding:0 0 6px 0; letter-spacing:0.1em; text-indent:0.1em; }
  1105. .font_grid_item h2 { font-size:${ UI_Prefs_Non_Bool.grid_font_size * 4 }em; font-weight:normal; }
  1106. .image_grid_item + .font_grid_item { margin-top:-1px; border-top:solid 1px hsl(0,0%,var(--border_lum_inverted)); }
  1107. /* CONTENT FONT.content */
  1108. #content_font { font-size:${ UI_Prefs_Non_Bool.grid_font_size }em; overflow-wrap:break-word; align-content:start; flex-direction:column; }
  1109. #content_pane.has_font_specimen #content_container, .has_font_specimen_glyph #content_container, .has_font_specimen_glyph #font_specimen, .has_font_file_glyph #content_container { overflow:hidden; }
  1110. #content_pane[data-content*="has_font"] #font_toolbar { display:grid; overflow-x:scroll; overflow-y:hidden; }
  1111. #content_pane.has_font_specimen #font_specimen_variants { display:flex; } #content_pane.has_font_specimen #font_specimen_adjustments li.display_none { display:unset; }
  1112. #content_pane.has_font_file_glyph #content_font { background-color:hsl(0,0%,var(--percent_100)); }
  1113. #font_toolbar li { margin:2px; padding:0 4px; white-space:pre; }
  1114. #font_toolbar li.text { width:50%; font-size:var(--font_size_small); }
  1115. #font_variant_select { width:13em; }
  1116. #unicode_char_ranges_select { width:12em; }
  1117. #font_specimen_adjustments { font-size:0.75rem; padding-bottom:2px; }
  1118. #font_specimen_adjustments input { width:8em; }
  1119. #font_specimen_viewer { min-width:100%; flex-direction:column; }
  1120. #font_specimen_viewer .specimen { padding:20px; outline:none; color:inherit; font-weight:normal; }
  1121. #font_specimen_viewer .specimen:focus, #font_specimen_viewer .specimen:focus-visible { box-shadow:inset 0 0 2px 2px hsl(212deg 50% 60%); border-radius:3px; outline:none !important; }
  1122. .specimen:focus, specimen:focus-visible { background-color: hsl(0,0%,var(--percent_100)); }
  1123. #font_specimen_grid:empty, #font_specimen_glyph:empty, #font_specimen_grid:empty + hr { display:none; }
  1124. .font_glyph_item { grid-column:auto; display:flex; justify-content:center; position:relative; cursor:pointer; -webkit-text-stroke:inherit !important; letter-spacing:initial; line-height:initial; }
  1125. .font_glyph_item div { color:inherit; }
  1126. .font_glyph_item::before, .font_glyph_item::after, #font_specimen_viewer::before, #font_specimen_viewer::after { display:inline-block; position:absolute; font-size:0.75rem; top:0; font-family:${UI_Prefs_Non_Bool.ui_font}; opacity:0.75; font-feature-settings:normal; font-variant:normal; line-height:1.2; letter-spacing:normal; -webkit-text-stroke:0 !important; }
  1127. .font_glyph_item::before, #font_specimen_viewer::before { content:attr(data-unicode_dec); left:2px; }
  1128. .font_glyph_item::after, #font_specimen_viewer::after { content:attr(data-unicode_hex); right:2px; }
  1129. .has_font_specimen_glyph #font_specimen_viewer::before, .has_font_specimen_glyph #font_specimen_viewer::after, .has_font_file_glyph #font_file_glyph_viewer::before, .has_font_file_glyph #font_file_glyph_viewer::after { display:inline-block; padding:4px 6px; font-size:0.875rem; position:fixed; white-space:pre; opacity:1; z-index:2; }
  1130. #font_specimen_1 { font-size:4em;} #specimen_2 { font-size:8em; } #specimen_3 { font-size:6em; } #specimen_2H4 { font-size:1.618em; } #specimen_3H3 { font-size:2em; }
  1131. .lorem { font-size:1em; column-gap:1.5em; overflow-wrap:normal; }
  1132. #lorem::first-line { letter-spacing:0.1em; text-indent:0.1em; font-size:${ UI_Prefs_Non_Bool.grid_font_size * 1.33 }em; font-variant:small-caps; }
  1133. #lorem_2 { padding:12px 0 0; columns:2; }
  1134. #lorem_3 { padding:12px 0 0; columns:3; }
  1135. /* FONT GLYPHS */
  1136. #font_specimen_viewer, #font_file_viewer { line-height:1.5; background-color:inherit; }
  1137. .glyph_container:hover, .glyph_container.selected { z-index:1; }
  1138. #font_specimen_glyph, #font_specimen_glyph:not(:empty) + #font_specimen_glyph_overlay { display:flex; justify-content:center; font-size:64vw; overflow:visible; }
  1139. #font_file_viewer { font-family:unset; flex-direction:column; }
  1140. #content_pane.has_font_file_glyph #font_file_grid { visibility:hidden; }
  1141. #content_pane.has_font_file_glyph #font_file_viewer { overflow:hidden; }
  1142. #font_file_grid { margin-bottom:21px; }
  1143. .font_glyph_item svg { width:1em; height:1em; display:block; overflow:visible; }
  1144. .font_glyph_item g, #font_file_glyph_viewer svg g { transform-origin:center; }
  1145. #font_file_glyph_viewer { background-color:#FFF; }
  1146. #font_file_glyph_viewer::before, #font_file_glyph_viewer::after { position:absolute; color:hsl(0,0%,var(--percent_65)); }
  1147. #font_file_glyph_viewer::before { content:attr(data-unicode_dec); }
  1148. #font_file_glyph_viewer::after { content:attr(data-unicode_hex); right:0; }
  1149. .has_font_file_glyph #save_svg { display:initial; margin-right:4px; }
  1150. #font_info { max-height:${ (window.innerHeight * 0.75) }px; left:-1px; right:0; bottom:-1px; overflow-y:auto; }
  1151. #font_info:hover { box-shadow:0px 4px 6px 3px #333; }
  1152. /* OTHER CONTENT ELEMENTS */
  1153. #content_image_container { padding:2rem 2.5rem; box-sizing:border-box; }
  1154. .has_zoom_image #content_image_container, .has_scaled_image #content_image_container { padding:0; }
  1155. #content_image { margin:auto; width:auto; max-width:100%; height:auto; max-height:100%; object-fit:contain; cursor:zoom-in; }
  1156. #content_image:focus-visible { outline:none; }
  1157. #content_pane.has_zoom_image #content_image { width:fit-content; height:fit-content; max-width:unset; max-height:unset; cursor:zoom-out; }
  1158. #content_video { background:transparent; height:auto; max-width:calc(100% - 4em); max-height:calc(100% - 4em); }
  1159. #content_iframe { background:white; }
  1160. #content_pane.has_emptycontent #content_iframe { background:unset; }
  1161. `;
  1162. const conditional_styles = `
  1163. /* PSEUDO-ELEMENTS */
  1164. #reload_btn.reset::before, #content_pane:is(.has_font_specimen,.has_font_specimen_glyph,.has_font_file,.has_font_file_glyph,.has_zoom_image,.has_scaled_image) #reload_btn::before { content:"Reset"; }
  1165. .texteditor_edited #show_texteditor span:after, .iframe_edited:not(.has_texteditor) #content_pane.has_iframe #content_title div::after { content:" (edited)"; }
  1166. .theme_light #menu_theme span::before { content:"Light "; }
  1167. .theme_dark #menu_theme span::before { content:"Dark "; }
  1168. body:not(.show_details_false) #show::before { content:"Hide "; }
  1169. body.text_editing_enable_false #disable::after { content:"Disabled"; }
  1170. #disable::after { content:"Enabled"; }
  1171. .is_error #sidebar_header_utilities { border-bottom:0; }
  1172. #is_error { display:block !important; grid:none !important; grid-template-columns:none !important; }
  1173. .theme_dark #is_error_items, .theme_dark #sidebar_menu_main li > span::before, .theme_dark #sidebar_header_utilities_row_2 span::before, .theme_dark #sidebar_header_utilities_row_2 span::after, .theme_light #sidebar_menu_main li.selected > span::before { filter:invert(1); }
  1174. /* CONDITIONAL DISPLAY */
  1175. body:not(.alternate_background_false).is_error #alternate_background, .is_error #sidebar_header_utilities > div:not(:first-of-type), .is_non_local #show_invisibles_container { display:none; }
  1176. .has_media #play_toggle, .theme_dark #theme_dark, .theme_light #theme_light, #content_pane[class^="has_"] #close_btn, #content_pane[data-content="has_texteditor"] #close_btn { display:unset; }
  1177. .has_playlist #stats_summary_playlist_files, .has_filelist #stats_summary_playlist_files { display:table-row; }
  1178. #sidebar_footer li, .has_warning #overlay_container, .cuesheet_track_list_container.has_cue_sheet, .has_playlist #close_playlist_container, .has_filelist #close_playlist_container { display:block; }
  1179. .has_menu_stats .dirlist_item.invisible, .has_menu_stats .dirlist_item.ignored, .has_menu_stats .dirlist_item.ignored.hovered, body:not(.show_ignored_items_false).has_menu_stats .dirlist_item.ignored, body:not(.show_ignored_items) .dirlist_item.ignored:not(.invisible) { display:grid; }
  1180. #content_pane[data-content="has_texteditor"] .content_el.has_content, #content_pane[data-content="has_texteditor"] #content_grid, body:not(.has_texteditor) #content_pane[data-content="has_grid"] #content_texteditor, #content_pane[data-content="has_grid"] .content_el.has_content { display:none !important; }
  1181. `;
  1182. const texteditor_styles = `
  1183. html, body, #content_body { margin:0; padding:0; height:100%; overflow:hidden; position:relative; font-family:${ UI_Prefs_Non_Bool.ui_font }; font-size:${ UI_Prefs_Non_Bool.ui_font_size }; }
  1184. button.focus, button:focus { outline:none; border-radius:3px !important; border-style:solid !important; border-width:1px !important; border-color:#222 !important; }
  1185. .is_texteditor #content_texteditor, body.is_text #content_texteditor { display:flex; }
  1186. /* TOOLBAR */
  1187. #texteditor_toolbar { overflow:visible; z-index:100; font-size:${ parseFloat(UI_Prefs_Non_Bool.ui_font_size) * 0.875 + UI_Prefs_Non_Bool.ui_font_size.replace(/\d*/,'') }; }
  1188. .toolbar_icon { margin:0 4px; padding:4px; min-width:16px; height:16px; cursor:pointer; opacity:0.5; }
  1189. #texteditor_sync_scroll { opacity:1; height:24px; padding:0 8px; flex-grow:unset; }
  1190. #texteditor_sync_scroll input { margin:0 4px 0 0; z-index:-1; }
  1191. #save_btn ul { top:-3px; right:-4px; }
  1192. #save_html, #save_text { grid-column:1; }
  1193. #save_btn_icon { grid-column:2; grid-row:span 2; width:32px; }
  1194. #save_btn_icon svg { margin:3px; }
  1195. .texteditor_edited #save_btn svg { color:red !important; }
  1196. #texteditor_toolbar li:hover, .texteditor_view_raw #toggle_texteditor_view_raw, body:not(.texteditor_split_view_false) #toggle_texteditor_view_raw, body:not(.texteditor_split_view_false):not(.texteditor_view_html) #toggle_texteditor_view_styled, .texteditor_view_styled #toggle_texteditor_view_styled, .texteditor_view_html #toggle_texteditor_view_html, body:not(.texteditor_split_view_false) #toggle_texteditor_split_view { opacity:1; }
  1197. /* TEXT CONTENT CONTAINERS */
  1198. .texteditor_pane { padding:1em; overflow-y:scroll; box-sizing:border-box; background:transparent; font-size:${ parseFloat(UI_Prefs_Non_Bool.ui_font_size) + UI_Prefs_Non_Bool.ui_font_size.replace(/\d*/,'') }; }
  1199. body:not(.text_editing_enable_false) .texteditor_pane, body:not(.texteditor_split_view_false) .texteditor_pane { width:50%; }
  1200. body:is(.text_editing_enable_false,.texteditor_split_view_false) .texteditor_pane { width:100% !important; }
  1201. #text_container .texteditor_pane:focus { background-color:hsl(0,0%,var(--percent_90)); outline:none; box-shadow:inset 0px 0px 4px hsl(0,0%,var(--percent_95)); }
  1202. #text_container textarea { font-family:monospace; }
  1203. /* EDITOR PANES */
  1204. .texteditor_split_view_false.texteditor_view_raw #texteditor_raw_pane, .texteditor_split_view_false.texteditor_view_styled #texteditor_styled_pane,
  1205. .texteditor_split_view_false.texteditor_view_html #texteditor_html_pane, .texteditor_split_view_false.texteditor_view_html.texteditor_view_raw #texteditor_raw_pane,
  1206. .texteditor_split_view_false.texteditor_view_styled.texteditor_view_raw #texteditor_raw_pane,
  1207. .texteditor_split_view_true #texteditor_raw_pane, .texteditor_split_view_true:not(.texteditor_view_styled):not(.texteditor_view_html) #texteditor_styled_pane,
  1208. .texteditor_split_view_true.texteditor_view_styled #texteditor_styled_pane, .texteditor_split_view_true.texteditor_view_html #texteditor_html_pane,
  1209. .text_editing_enable_true #reading_btn_icon, .text_editing_enable_false:not(.texteditor_view_styled) #texteditor_raw_pane,
  1210. .text_editing_enable_false.texteditor_view_raw #texteditor_raw_pane, .text_editing_enable_false.texteditor_view_styled #texteditor_styled_pane
  1211. { display:block !important; }
  1212. .text_editing_enable_false:not(.has_texteditor) :is(#text_editing_handle,#texteditor_toolbar,#save_btn,#texteditor_sync_scroll,#toggle_texteditor_html_menu,#texteditor_html_pane),
  1213. .texteditor_split_view_false :is(#texteditor_sync_scroll,#text_editing_handle), .texteditor_split_view_false.texteditor_view_raw :is(#texteditor_styled_pane,#texteditor_html_pane),
  1214. .texteditor_split_view_false.texteditor_view_styled :is(#texteditor_raw_pane,#texteditor_html_pane), .texteditor_split_view_false.texteditor_view_html :is(#texteditor_raw_pane,#texteditor_styled_pane),
  1215. .texteditor_split_view_true.texteditor_view_styled #texteditor_html_pane, .texteditor_split_view_true.texteditor_view_html #texteditor_styled_pane
  1216. { display:none !important; }
  1217. /* THEMES & COLORS */
  1218. .texteditor_theme_default #content_texteditor .background_grey_95, .background_grey_90:focus, #texteditor_styled_pane table th, #text_container { background-color:hsl(0,0,var(--percent_95)); }
  1219. .theme_dark.texteditor_theme_light #content_texteditor .text_color_default, .theme_light.texteditor_theme_default #content_texteditor .text_color_default { color:hsl(0,0%,var(--percent_05)); }
  1220. .theme_dark #content_texteditor .text_color_default, .texteditor_theme_dark #content_texteditor .text_color_default { color:#EEE; }
  1221. /* custom previewed text styles */
  1222. #texteditor_styled_pane { word-break:break-word; }
  1223. #texteditor_styled_pane pre { font-size:${ parseFloat(UI_Prefs_Non_Bool.ui_font_size) + UI_Prefs_Non_Bool.ui_font_size.replace(/\d*/,'') }; border:solid 1px #CCC; border-radius:3px; white-space:pre-wrap; word-break:break-word; }
  1224. #texteditor_styled_pane th, #texteditor_styled_pane td { vertical-align:top; }
  1225. #texteditor_styled_pane blockquote { margin-top:1em; margin-bottom:1em; color:#555; }
  1226. #texteditor_styled_pane blockquote + blockquote { margin-top:0; }
  1227. #texteditor_styled_pane img { max-width:100%; height:auto; }
  1228. .markdown_body input[type="checkbox"] { margin-top:0.375em; margin-right:6px; float:left; }
  1229. h1 .uplink,h2 .uplink,h3 .uplink,h4 .uplink,h5 .uplink,h6 .uplink { display:inline-block; font-size:0.875em; transition:opacity 0.25s; opacity:0; cursor:pointer; margin:0; padding:0; }
  1230. h1:hover .uplink,h2:hover .uplink,h3:hover .uplink,h4:hover .uplink,h5:hover .uplink,h6:hover .uplink { transition:opacity 0.25s; opacity:0.5; }
  1231. #texteditor_styled_pane table { font-size:inherit; }
  1232. .markdown_body table tr, .markdown_body .highlight pre, .markdown_body pre { background-color:transparent !important; }
  1233. .markdown_body::before, .markdown_body::after { display:none !important; background:transparent; }
  1234. #content_body.has_warning::after { content:""; position:absolute; top:0; right:0; bottom:0; left:0; background:rgba(0,0,0,0.33); z-index:9998; }
  1235. #text_editing_handle { width:8px; top:0; bottom:0; left:calc(50% - 4px); cursor:col-resize; }
  1236. #text_editing_handle::before { content:""; width:1px; background:hsl(0,0%,var(--border_lum)); position:absolute; top:0; bottom:0; left:calc(50%); }
  1237. .texteditor_theme_dark #text_editing_handle::before, .theme_dark.texteditor_theme_default #text_editing_handle::before { background:#111; }
  1238. .is_link #texteditor_styled_pane a { font-size:0.875rem; } .is_link #texteditor_styled_pane a:hover { font-weight:bold; }
  1239. `;
  1240. // Gecko (Firefos) Styles:
  1241. const gecko_style_rules = `
  1242. .dir::before { content:"" !important; display:none !important; }
  1243. .is_gecko button { padding:revert; }
  1244. .is_gecko #show_grid_btn .menu { top:-7px; left:-120px; }
  1245. .is_gecko thead { font-size:100%; }
  1246. .is_gecko .dirlist_item.dir::before { position:absolute; }
  1247. .is_gecko .dirlist_item_name span { display:-webkit-box; width:auto; white-space:normal; }
  1248. .dirlist_item.dir td:not(:first-child), .dirlist_item.file td:not(:first-child) { width:unset !important; }
  1249. .is_gecko .dirlist_item td { min-width:calc(100% - 24px); }
  1250. .is_gecko .dir::before { content:"" !important; display:none !important; }
  1251. .is_gecko.use_default_icons:not(.is_converted_list) .dirlist_item.file .icon { padding-left:4px; background:none; }
  1252. .is_gecko.use_default_icons .dirlist_item.file .icon img { margin-right:6px; height:14px; }
  1253. .is_gecko #directory_list > tr > td:not(:first-of-type) { float:left }
  1254. .is_gecko #content_audio_title span { padding-top:6px;, padding-bottom:0; }
  1255. .is_gecko #audio,.is_gecko #audio_container { background-color:rgba(26,26,26,1); }
  1256. .is_gecko #prev_track, .is_gecko #next_track, .is_gecko #close_audio { filter:invert(1); border:none !important; }
  1257. .is_gecko #content_pane.has_zoom_image #content_image_container { display:block !important; }
  1258. `;
  1259. const safari_style_rules = `
  1260. .is_safari button { background-color:#FFF; }
  1261. .is_safari.theme_dark #prev_track, .is_safari.theme_dark #next_track, .is_safari.theme_dark #close_audio { filter:invert(1); }
  1262. `;
  1263. const chrome_style_rules = `video::-webkit-media-controls-enclosure { border-radius:0 !important; }`;
  1264. const html_style_rules = `a:focus, a:focus-visible { font-weight:bold; border-radius:1px; outline:currentcolor solid 1px; outline-offset:1px; display:inline-block; padding:0 2px; text-decoration:none; }`
  1265. //==============================//
  1266. function addStyles(user_agent) { // ===> ADD STYLES
  1267. let default_styles = `<style id="main_styles">${ global_styles } ${ sidebar_styles } ${ content_pane_styles } ${ utilities_styles }</style> <style id="conditional_styles">${ conditional_styles }</style> <style id="font_styles"></style> <style id="font_grid_styles"></style>`;
  1268. switch(user_agent) {
  1269. case user_agent === 'is_gecko': default_styles += `<style id="gecko_style_rules">${ gecko_style_rules }</style>`; break;
  1270. case user_agent === 'is_safari': default_styles += `<style id="safari_style_rules">${ safari_style_rules }</style>`; break;
  1271. case user_agent === 'is_chrome': default_styles += `<style id="chrome_style_rules">${ chrome_style_rules }</style>`; break;
  1272. }
  1273. return default_styles // return styles
  1274. }
  1275. // ***** END STYLES ***** //
  1276. //==============================//
  1277. // ***** INDEX PREP ***** //
  1278. // Try to determine index type from parent directory link container, with fallbacks for indexes that don't have parent directories, or for parent directory links that aren't siblings or ancestors of the index itself.
  1279. function getIndexType() { // ===> GET INDEX TYPE
  1280. let index_el = getEls('body > ul, body ul, body > pre, body > table:last-of-type, body div table');
  1281. if ( index_el.length > 1 ) { index_el = ( Array.from(index_el).filter( el => el?.nodeName?.toLowerCase() === 'table') || index_el.reverse()[0] ) } // some index pages have pre and table elements; list is usually table
  1282. index_el = index_el[0];
  1283. let node_name = ( index_el !== undefined ? index_el.nodeName.toLowerCase() : 'body' ); // "body" is likely to be an error page
  1284. let types = {'gecko':'gecko','ul':'list','pre':'pre','table':'table','th':'table','td':'table','div':'default','error':'error','body':'error','permission_denied':'permission_denied'}; // object array of types
  1285. return types[node_name]; // return index type
  1286. }
  1287. function getIndexItems(agent) { let type = getIndexType(agent), items; // ===> GET INDEX ITEMS // get index type, define items
  1288. switch(type) {
  1289. case 'error': items = document.getElementsByTagName('html')[0].outerHTML; break; // error type
  1290. case 'pre': items = getEl('body > pre').innerHTML; break; // pre type
  1291. case 'list': items = getEls('body > ul li, body > * > ul li'); break; // list type
  1292. case 'table': case 'td': // table types
  1293. switch(true) {
  1294. case elExists('table > tbody'): items = getEls('body table > tbody tr'); break; // ordinary tables
  1295. case !elExists('table > tbody'): items = getEls('body table tr'); break; // tables without tbody element
  1296. }
  1297. break;
  1298. case 'gecko': items = getEls('body > table > tbody > tr'); break; // gecko type
  1299. case 'default': items = getEls('body > table > tbody tr'); break; // default: how is this different from table type?
  1300. }
  1301. return [items,type]; // return index items and index type
  1302. }
  1303. //==============================//
  1304. function prepPreType(items_str) { // ===> PREP PRE TYPE
  1305. let prepped_index = [], parser = new DOMParser(), items_HTML = parser.parseFromString(items_str, "text/html"); // convert items_str to DOM html
  1306. items_HTML.querySelectorAll('hr,img').forEach( el => el.remove() ); // remove junk elements
  1307. items_HTML.querySelectorAll('a').forEach( // remove junk links (sorting and parent links) or define item_link
  1308. el => { if ( /^\?|^\./m.test(el.getAttribute('href')) || /^Parent$|^Parent Directory$|^\s*Up\s*$|^\s*Root\s*$/im.test(el.innerText) ) { el.remove(); } }
  1309. );
  1310. items_str = items_HTML.querySelector('body').innerHTML; // convert DOM html back to str
  1311. // remove header elements | link text nodes | links with empty text nodes (which are sometimes duplicated) | name, last modified, size, description)
  1312. items_str = items_str.replace(/\&lt;dir\&gt;/gm,' ').replace(/<br>/gi,'\n').replace(/[ ]*<h\d>[^<]*<\/h\d>[ ]*/gmi,'').replace(/[ ]*(<a[^>]+?>)[^<]*(<\/a>)/g,'$1$2 ').replace(/(\w)<a /g,'$1 <a '); // clean string
  1313. const items = items_str.split('\n'); // create array of item strings from items
  1314. for ( let i = items.length; i--; ) {
  1315. let prepped_item = [], link;
  1316. let cells = items[i].split(/\s{2,}/);
  1317. for ( let j = cells.length; j--; ) {
  1318. let cell = cells[j];
  1319. if ( cell.trim().length > 0 && cell.trim() !== '-' ) {
  1320. if ( !cell.startsWith('<a ') ) { prepped_item.push(cell); } else { link = cell.split('"')[1]; } // extract link
  1321. }
  1322. }
  1323. if ( link === undefined || ( /^\.\.$|^\.\.\/$|^\/$|^\?|\?sort=|\?path=\&/mi.test(link) ) ) { prepped_item = []; } else { prepped_item.unshift(link); } // exclude some items (e.g., parent directory links)
  1324. if ( prepped_item.length > 0 ) { prepped_index.push(prepped_item); } // add prepped item to index
  1325. }
  1326. return prepped_index; // return prepped index
  1327. }
  1328. function prepListType(items) { // ===> PREP LIST TYPE
  1329. let prepped_index = [];
  1330. for ( let i = items.length; i--; ) {
  1331. let item = items[i];
  1332. if ( item.innerHTML.indexOf('Parent Directory') === -1 ) {
  1333. let prepped_item = [], link = item.querySelector('a')?.href;
  1334. item.querySelector('a')?.remove(); Array.from(item.children).forEach( child => { if ( child.innerText === '' ) { child.remove(); } }) // remove link and empty child elements
  1335. let cells = item.innerHTML.split(' '); // create array from remaining elements
  1336. for ( let cell of cells ) { prepped_item.push(cell); }
  1337. if ( link === undefined || ( /^\.\.$|^\.\.\/$|^\/$|^\?|\?sort=|\?path=\&/mi.test(link) ) ) { prepped_item = []; } else { prepped_item.unshift(link); } // exclude some items (e.g., parent directory links)
  1338. if ( prepped_item.length > 0 ) { prepped_index.push(prepped_item); }
  1339. }
  1340. }
  1341. return prepped_index; // return prepped index
  1342. }
  1343. function prepGeckoType(items) { // ===> PREP GECKO TYPE
  1344. let prepped_index = [];
  1345. for ( let item of items ) {
  1346. let prepped_item = [], cellContents = '', cells = item.cells, link = item.innerHTML.split('href=\"')[1].split('\">')[0];
  1347. for ( let cell of cells ) {
  1348. cellContents = cell.innerText;
  1349. cellContents = ( cellContents !== undefined ? cellContents.trim() : '');
  1350. prepped_item.push(cellContents);
  1351. }
  1352. prepped_item[1] = prepped_item[1].replace(/\s*KB/,'000'); // convert reported size in KB to total bytes
  1353. prepped_item[2] = prepped_item[2] + ' '+ prepped_item[3];
  1354. prepped_item = prepped_item.slice(1,-1);
  1355. if ( link === undefined || ( /^\.\.$|^\.\.\/$|^\/$|^\?|\?sort=|\?path=\&/mi.test(link) ) ) { prepped_item = []; } else { prepped_item.unshift(link); } // exclude some items (e.g., parent directory links)
  1356. if ( prepped_item.length > 0 ) { prepped_index.push(prepped_item); }
  1357. }
  1358. return prepped_index; // return prepped index
  1359. }
  1360. function prepTableType(items) { //*** for local chrome indexes and server-generated table-type indexes // ===> PREP TABLE TYPE
  1361. // const testString = new RegExp(/alt=\"\[PARENTDIR\]|>\s*\&nbsp;\s*<|^\s*\&nbsp;\s*$|^\s*-\s*$|\?sort=|\?path=\&/,'mi');
  1362. let prepped_index = [], prepped_item, item, cell, cell_text;
  1363. for ( item of items ) {
  1364. if ( item.querySelector('td a') !== null ) { let link; prepped_item = []; // get legitimate items (i.e., those containing a link)
  1365. for ( cell of item.cells ) { // get text from remaining cells (date & size)
  1366. switch(true) {
  1367. case cell.querySelector('a') !== null && link === undefined: link = item.querySelector('a')?.getAttribute('href'); // get link; add to prepped_item; ignore if link already defined
  1368. if ( !/^\?|^\.\.\/$|^\|\"\/\".$/m.test(link) && !/^\s*parent directory\*$|^\*up\s*$/m.test(item.innerText.toLowerCase()) ) { prepped_item.unshift(link); }// else { prepped_item.unshift(''); }
  1369. break;
  1370. default:
  1371. cell_text = cell.innerText.trim().replace(/(^[ ]*-[ ]*$|[ ]*-[ ]*\&nbsp;[ ]*$)|\&nbsp;/m,''); // prep cells and clean cell text
  1372. if ( !/<td\s*[^>]*>dir|directory|file<\/td>|>\w*\s*file<|>\w*\s*unknown</i.test(cell.outerHTML.toLowerCase()) && cell_text !== '' ) { prepped_item.push( cell_text ); } // exclude various cells
  1373. }
  1374. }
  1375. if ( prepped_item.length > 1 && prepped_item[0] !== '' ) { prepped_index.push(prepped_item); } // prepped_item.length > 2 in order to omit parent directory item
  1376. }
  1377. }
  1378. return prepped_index; // return prepped index
  1379. }
  1380. function prepErrorType(items) { return items; } // ===> PREP ERROR TYPE; receives and returns html string
  1381. function prepPlaylist(items) { // ===> PREP PLAYLIST items
  1382. let prepped_index = []; let prepped_item, link, duration, name, info; let items_arr, type;
  1383. items = items.replace(/\s*#EXTM3U.*\s*/g,'').replace(/^\*\n{2,}/gm,'\n').replace(/\.pdf\?.+?\n/g,'.pdf\n');//.replace(/\?/g,'%3F'); // remove header comment and multiple returns
  1384. switch(true) { // determine playlist type;
  1385. case ( /#EXTINF:/i.test(items) ): type = 'extm3u'; items_arr = items.split('#EXTINF:'); break; // rows made by splitting at "#EXTIMG:" prefix
  1386. default: type = 'm3u'; items_arr = items.split('\n'); break; // rows are just naked links
  1387. }
  1388. items_arr.forEach( (item) => {
  1389. switch(true) { // get entry information: title, link, etc.
  1390. case type === 'extm3u': item = item.trim().split('\n'); link = item[1]; info = item[0].split(','); duration = info.shift(); name = info.join(',');
  1391. if ( item[1] !== undefined ) { prepped_item = [link,duration,'',name]; } break;
  1392. case type === 'm3u': prepped_item = [item,'','']; break; // m3u with urls only
  1393. }
  1394. if ( prepped_item !== undefined ) { prepped_index.push(prepped_item); }
  1395. });
  1396. return prepped_index; // return prepped index
  1397. }
  1398. function convertIndexItems(items,type) { let converted = []; // ===> CONVERT INDEX ITEMS by type; returns [prepped_index]
  1399. switch(type) {
  1400. case 'gecko': converted = prepGeckoType(items); break;
  1401. case 'list': converted = prepListType(items); break;
  1402. case 'pre': converted = prepPreType(items); break;
  1403. case 'table':
  1404. case 'default': converted = prepTableType(items,type); break;
  1405. case 'error': converted = prepErrorType(items); break;
  1406. }
  1407. return converted;
  1408. }
  1409. //==============================//
  1410. function buildNewIndex(id,prepped_index,sort,type,body_id) { // ===> BUILD NEW INDEX from prepped rows
  1411. let i, new_index_items = [], body_classes = new Set();
  1412. let index_html = '';
  1413. let new_item, item, item_info = [], item_link, item_name, item_sort_name, item_size_and_date, item_size, item_sort_size, item_date, item_sort_date, item_ext, item_description, item_sort_kind, item_classes;
  1414. let name_span, cell_link, cell_name, cell_size, cell_date, cell_kind, cell_ext, cell_time, prepped_index_length = prepped_index.length, item_disabled, item_input;
  1415. let stats, stats_classes = [], stats_kinds = [], stats_total_size = 0, media_count = 0;
  1416. let dir_list_parent_class = ( (body_id === ( null || 'top_body') || type === 'playlist') ? 'top_item' : 'iframe_item' ), is_playlist = (type === 'playlist' ? type +'_' : ''); // id used to set dir list details style
  1417. let parent_id = ( getCurrentUIPref('parent_id') || '' ), connector = ( getCurrentUIPref('parent_id') ? '_' : '' ), level = ( Number(getCurrentUIPref('level')) || 0 ), level_style = ( level === 0 ? '' : `style="padding-left:${ Number(level) * 22 }px;"` ); // ensure unique ids (with parent_id) and set indents for subdirectory items
  1418. if ( prepped_index_length > 5000 ) { if ( confirm(`This directory contains ${prepped_index_length} items; it may take a long time to process and could cause your browser to crash. Are you sure you want to open it?`) === false ) { return } }
  1419. switch(type) { // add body classes according to index type
  1420. case 'error': body_classes.add('is_error'); break;
  1421. case 'pre': body_classes.add('is_converted_pre'); break;
  1422. case 'list': body_classes.add('is_converted_list'); break;
  1423. case 'table': case 'td': body_classes.add('is_converted_table'); break;
  1424. case 'default': body_classes.add('is_default'); break;
  1425. }
  1426. // create and format directory item
  1427. for ( i = 0; i < prepped_index_length; i++ ) {
  1428. item = prepped_index[i];
  1429. item_info = getLinkInfo(item[0]); // returns [link,name,ext,kind,item_classes,body_classes];
  1430. item_link = item_info[0];
  1431. item_name = item_info[1] || item[3]; // prep display name, with word breaks added after unbreakable chars
  1432. item_sort_name = item_name.toLocaleLowerCase();
  1433. item_size_and_date = getItemSizeAndDate(item);
  1434. item_size = item_size_and_date[0];
  1435. item_sort_size = item_size_and_date[1];
  1436. item_date = item_size_and_date[2];
  1437. item_sort_date = item_size_and_date[3];
  1438. item_ext = item_info[2];
  1439. item_sort_kind = item_info[3];
  1440. item_classes = item_info[4] +" "+ dir_list_parent_class;
  1441. item_disabled = ( /local/.test(item_classes) ? ' disabled="disabled"' : '' ); // disable media if local file on non_local page or vice versa
  1442. item_input = ( /audio|video/.test(item_sort_kind) && /top/.test(body_id) || type === 'playlist' ? `<input class="dirlist_item_input" type="checkbox" tabindex="-1" checked="true" ${item_disabled} autocomplete="off" />` : '' );
  1443. // Assemble item elements
  1444. name_span = `<span class="icon has_icon_before_before"></span><span class="name_span display_flex">${ item_input }<span>${ item_name }</span></span>`;
  1445. cell_link = `<a href="${ item_link }" class="icon dirlist_item_name name dirlist_item_name_a position_relative">${ name_span }</a>`;
  1446. cell_name = `${ cell_link }`;
  1447. cell_time = `<span class="dirlist_item_media_duration align_right" data-duration=""></span>`;
  1448. cell_size = `<span class="dirlist_item_details size details" data-size="${ item_sort_size }">${ item_size }</span>`;
  1449. cell_date = `<span class="dirlist_item_details date details overflow_hidden" data-date="${ item_sort_date }">${ item_date }</span>`;
  1450. cell_kind = `<span class="dirlist_item_details kind details" data-kind="${ item_sort_kind }">${ item_sort_kind }</span>`;
  1451. cell_ext = `<span class="ext details" data-ext="${ item_ext }"></span>`;
  1452. item_description = ( item[3] !== undefined ? `<span class="desc dirlist_item_details"><i>Description</i>: ${ item[3] } </span>` : '' ); // some servers provide a description of the item
  1453. // Assemble item
  1454. new_item = `<li id="${ parent_id }${ connector }${ is_playlist }rowid-${ (prepped_index.length - i) }" class="dirlist_item ${ item_classes }" data-title="${ item_name }" data-name="${ item_sort_name.split("/")[0] }" data-kind="${ item_sort_kind }" data-ext="${ item_ext }" data-level="${ level }" ${ level_style }>${ cell_name } ${ cell_time } ${ cell_size } ${ cell_date } ${ cell_kind } ${ cell_ext } ${ item_description }</li>\n`;
  1455. if ( /audio|video/.test(item_sort_kind) ) { media_count += 1; // if media item...
  1456. let media_kind = item_sort_kind, media_item_id = parent_id + connector + is_playlist + 'rowid-'+ ( prepped_index.length - i), is_subdir = ( /subdirectory/.test(window.location.search) ? true : false );
  1457. if ( media_count < 1000 ) { getMediaDuration( item_link, media_kind, media_item_id, is_subdir ); } else { new_item = new_item.replace(/data-duration="">/,'data-duration="NaN">'); } // get media duration (limit to 1000 calls)
  1458. }
  1459. new_index_items.push(new_item); // add item to index items
  1460. body_classes.add(item_info[5].join(' ')); // add item classes to body_classes
  1461. stats_kinds.push(item_sort_kind); stats_total_size += Number(item_sort_size); stats_classes.push(item_info[6]); // STATS: add item kind; update total size; add to stats classes
  1462. }
  1463. body_classes = [...body_classes].filter(body_class => body_class).sort(); // BODY CLASSES: body_classes to array, filter empty items, sort
  1464. stats = buildStats(stats_classes,stats_kinds,stats_total_size); // STATS: build stats
  1465. if ( sort === '' || sort === undefined ) { sort = getCurrentUIPref('sort_by'); } // SORT ITEMS: get sort_by pref
  1466. let sort_direction = getCurrentUIPref('sort_direction'); // get sort_direction pref
  1467. let sorted_index_items = sortDirListItems(new_index_items, 'sort_by_'+ sort, sort_direction); // make initial sort
  1468. return [sorted_index_items, body_classes.join(' '),stats,index_html]; // RETURN [sorted_index_items, body_classes, stats, index_html]
  1469. }
  1470. //==============================//
  1471. function getLinkInfo(link) { // ===> GET LINK INFO; returns [link,name,ext,kind,item_classes,body_classes,link_protocol]
  1472. switch(true) {
  1473. case link === undefined: return; // return if link undefined
  1474. case link === null: link = getEl('#content_iframe').src; break; // link from opening local link files links in iframe
  1475. case link.startsWith('file://') && window.location.protocol === 'file:': link = link.split('file://')[1]; break; // local links
  1476. case link.startsWith('/') && window.location.protocol === 'file:': link = 'file://'+ link; break; // local links
  1477. case !link.startsWith('/') && !link.endsWith('/') && !/\./.test(link): link = '/'+ link +''; break;
  1478. }
  1479. link = link.replace(/%3C/g,'\&lt;').replace(/\.pdf\..+/,'.pdf'); // fix and sanitize links
  1480. // if ( /\.php\?(\w+)=/.test(link) ) { link = link.split('\.php')[1]; } // attempt to deal with some php links
  1481. let URL = newURL(decodeURIComponentSafe(encodeURIComponent(link)));
  1482. let prepped_link, display_name, kind, ext, item_classes = [], body_classes = [], stats_classes = [], aliases = new RegExp(/(symlink|alias|symbolic link)$/,'m'), link_protocol = URL.protocol;
  1483. switch(true) { // prep link
  1484. case window_protocol !== 'file:': // for non-local pages
  1485. switch(true) {
  1486. case URL.protocol === 'file:': case URL.protocol === undefined: prepped_link = link; item_classes.push('local','ignored'); break; // local links from non-local pages
  1487. default: prepped_link = URL.href; // non-local pages
  1488. } break;
  1489. case window_protocol === 'file:': // for non-local pages
  1490. switch(true) {
  1491. case URL.protocol !== 'file:': prepped_link = URL.href; item_classes.push('non_local'); break;
  1492. default: prepped_link = URL.pathname;
  1493. }
  1494. }
  1495. switch(true) { // prepare display name, body_classes, and item_classes
  1496. case ( /youtube.com|youtu.be/.test(link) && !link.indexOf('/.') ):
  1497. prepped_link = link.replace(/watch%3F/,'watch?'); kind = 'video'; item_classes.push('video','media'); display_name = undefined; break; // youtube videos from playlists
  1498. case URL.pathname.endsWith('/'): case ( /\.php\?/.test(link) ): // nobreak; dirs, apps and index.php? links
  1499. display_name = ( URL.pathname.endsWith('/') ? URL.pathname.split('/').reverse()[1] + '/' : /\.php\?/.test(link) ? link : null );
  1500. switch(true) {
  1501. case ( /\.app$|\.app\/$|\.exe$/m.test(display_name) ): ext = 'app'; kind = ext; // apps
  1502. if ( UI_Prefs_Bool.apps_as_dirs === false ) { item_classes.sort().unshift('file','app'); } else { item_classes.sort().unshift('dir','app'); } break;
  1503. default: ext = 'dir'; kind = 'dir'; item_classes.unshift(kind); // dirs; remove kind from item_classes
  1504. }
  1505. item_classes.push('non_media'); // add "non_media" to item_classes
  1506. if ( display_name.startsWith('.') ) { item_classes.push('invisible'); stats_classes.push('invisible'); } break;
  1507. default: // files
  1508. display_name = prepped_link.trim().split('/?')[0].split('/').reverse()[0];
  1509. switch(true) {
  1510. case display_name.toLowerCase().endsWith('symlink'): ext = 'symlink'; break;
  1511. case !/\./.test(display_name): ext = display_name.toLowerCase(); break; // if no '.' in link (typical for bin files), ...
  1512. default: // find the last . and get the remaining characters
  1513. ext = display_name.slice(display_name.lastIndexOf('.') + 1).toLowerCase();
  1514. for ( let item_kind in Item_Kinds ) { if ( Item_Kinds[item_kind].includes( ext ) ) { kind = item_kind; } } // kind = types
  1515. if ( /url|url\/|webloc|webloc\//.test(ext) ) { kind = 'link'; } // links
  1516. switch(true) {
  1517. case kind === 'audio': item_classes.push('media'); body_classes.push('has_media','has_audio'); break;
  1518. case kind === 'video': item_classes.push('media'); body_classes.push('has_media','has_video'); break;
  1519. case kind === 'font' : body_classes.push('has_fonts'); break;
  1520. case kind === 'image': body_classes.push('has_images'); break;
  1521. }
  1522. if ( Item_Settings.ignored.includes( ext ) ) { item_classes.push('ignored'); stats_classes.push('ignored'); }
  1523. if ( display_name.startsWith('.') ) { item_classes.push('invisible'); stats_classes.push('invisible'); }
  1524. }
  1525. if ( kind === undefined ) { kind = 'other'; }
  1526. if ( !/audio|video/.test(kind) ) { item_classes.push('non_media'); }
  1527. item_classes.unshift(kind); item_classes.unshift('file');
  1528. prepped_link = decodeURIComponentSafe(encodeURIComponent(prepped_link))?.trim();
  1529. }
  1530. stats_classes.push(kind);
  1531. if ( ext === undefined ) { ext = ''; }
  1532. if ( aliases.test(display_name) ) { item_classes.push('alias'); }
  1533. for ( let item_kind_system of Item_Kinds.system ) { if ( display_name?.endsWith(item_kind_system) ) { item_classes.push('ignored'); } } // ignore various system items
  1534. item_classes = Array.from(new Set(item_classes)).filter(item => item).join(' '); // remove dupe or empty classes, join
  1535. return [prepped_link,decodeURIComponentSafe(display_name)?.trim(),ext,kind,item_classes,body_classes,stats_classes.join(' '),link_protocol,URL.origin + URL.pathname];
  1536. }
  1537. //==============================//
  1538. function getItemSizeAndDate(item) { // ===> GET ITEM SIZE AND DATE
  1539. let item_size_and_date = [], item_display_size, item_sort_size, item_display_date, item_sort_date, size_units = /[BYTES|B|K|KB|MB|GB|TB|PB|EB|ZB|YB]/;
  1540. if ( item.length > 1 ) { // test for typical date/time separators.
  1541. if ( /[-:\/]/.test(item[1]) ) { item_display_date = item[1]; item_display_size = item[2]; } else { item_display_date = item[2]; item_display_size = item[1]; }
  1542. }
  1543. switch(true) { // get size
  1544. case item_display_size !== undefined && item_display_size.toLowerCase() === 'dir': case ( /undefined|—|-|,|\*/.test(item_display_size) ): case item_display_size === '':
  1545. item_display_size = '&mdash;'; item_sort_size = '0'; break; // if size is undefined, empty, or punctuation, use these defaults
  1546. default:
  1547. item_sort_size = getItemSortSize(item_display_size);
  1548. switch(true) {
  1549. case !item_display_size.toUpperCase().match(size_units) : item_display_size = formatBytes(item_display_size,1); break; // if provided size is only numeric, format byte size
  1550. default: item_display_size = item_display_size.replace('K','k').replace(/(\d+)\s*([A-z])/,'$1 $2'); // default: format and ensure display size has space between number and units
  1551. } break;
  1552. }
  1553. if ( item_display_size === 'NaN undefined' ) { item_display_size = '0 B'; }
  1554. // get date
  1555. if ( [undefined,'','-'].includes(item_display_date) ) { item_display_date = '&mdash;'; item_sort_date = '0'; } else { item_sort_date = getItemDate(item_display_date); }
  1556. item_display_date = item_display_date.replace(/, (.+)/,'<wbr>,&nbsp$1').replace(/ (AM|PM)$/im,'<wbr> $1').replace(/\s/g,'&nbsp;'); // ensure that time acts as a block for wrapping in narrow sidebar
  1557. item_size_and_date.push( item_display_size, item_sort_size, item_display_date, item_sort_date );
  1558. return item_size_and_date;
  1559. }
  1560. function getItemSortSize(val) { // GET ITEM SORT SIZE
  1561. let sort_size, values = val.replace(/(\d+)\s*([A-z]+)/,'$1 $2').split(' '), size = values[0], unit = values[1];
  1562. const factor = { undefined:1, '':1, B:1, K:1000, KB:1000, M:1000000, MB:1000000, G:1000000000, GB:1000000000, T:1000000000000, TB:1000000000000, P:1000000000000000, PB:1000000000000000, E:1000000000000000000, EB:1000000000000000000, Z:1000000000000000000000, ZB:1000000000000000000000 }; // unit to file size
  1563. if ( unit !== undefined ) { unit = unit.toUpperCase(); }
  1564. sort_size = size * factor[unit]; // convert byte size to multiplication factor
  1565. return sort_size;
  1566. }
  1567. function formatBytes(val, decimals) { // ===> FORMAT BYTES: format numeric sizes for display
  1568. const k = 1024, dm = (decimals < 0 ? 0 : decimals), sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], i = Math.floor(Math.log(val) / Math.log(k));
  1569. if (val === 0) { return '0 Bytes'; } else { return parseFloat((val / Math.pow(k, i)).toFixed(dm)) +' '+ sizes[i]; }
  1570. }
  1571. function processDate(match,p1,p2,p3) { //***date formats: 2017-10-09 13:12 || 2015-07-25T02:02:57.000Z || 12-Mon-2017 21:11 ***// // ===> PROCESS DATE
  1572. const mo = 'JanFebMarAprMayJunJulAugSepOctNovDec'.indexOf(p2)/3 + 1; // e.g., convert month into number, or use number
  1573. return p3 +'-'+ mo +'-'+ p1; // return assembled date: YYYY-MM-DD
  1574. }
  1575. function getItemDate(val) { // ===> GET ITEM DATE: for sorting (YYYY-MM-DD)
  1576. let sort_date = val.replace(/^(\d{2})-(\w{3})-(\d{4})/m, processDate) // convert Month to number
  1577. .replace(/\b(\d{1})[-:/]/g,'0$1/') // add leading 0 for single digit numbers
  1578. .replace(/(\d{2})\/(\d{2})\/(\d{2}),/,'$3$1$2') // reorder MM/DD/YY dates to YY/MM/DD
  1579. .replace(/-|:|\s+|\//g,''); // remove spacing characters
  1580. return sort_date;
  1581. }
  1582. //==============================//
  1583. // FETCH MEDIA DURATIONS
  1584. var getFormattedDuration = (secs) => { if ( isNaN(secs) ) { return Number.NaN; } // ===> GET FORMATTED TIME
  1585. let sec_num = parseInt(secs, 10), hours = Math.floor(sec_num / 3600), minutes = Math.floor(sec_num / 60) % 60, seconds = sec_num % 60;
  1586. let formattedTime = [hours,minutes,seconds].map( v => v < 10 ? "0" + v : v ).filter( (v,i) => v !== "00" || i > 0 ).join(":");
  1587. formattedTime = formattedTime.replace(/^0/m,''); return formattedTime; // remove initial 0 and return formatted time
  1588. };
  1589. async function fetchMediaDuration(link,kind) { // ===> ASYNC FETCH MEDIA DURATION
  1590. return new Promise((resolve, reject) => {
  1591. const media = ( kind === 'audio' ? new Audio() : document.createElement('video') );
  1592. media.addEventListener('loadedmetadata', () => { resolve(media.duration); }); media.addEventListener('error', reject); media.src = link.replace(/\&amp;/g,'&');
  1593. });
  1594. }
  1595. async function getMediaDuration(link,media_kind,id,is_subdir) { // ===> ASYNC GET MEDIA DURATION
  1596. try { const duration = await fetchMediaDuration(link,media_kind); // await media duration
  1597. if ( is_subdir === true ) { messageSend('top_body','set_media_duration','', [ id,media_kind,duration ] ); } else { setMediaDuration( id,media_kind,duration ); } // if subdir, send message, else set media duration
  1598. } catch (error) {
  1599. if ( id?.indexOf('playlist') && hasClass('body','has_playlist') || !id?.indexOf('playlist') && !hasClass('body','has_playlist') ) { setMediaDuration( id,media_kind,Number.NaN ); } // on error, set dur = NaN
  1600. }
  1601. }
  1602. // SET MEDIA DURATIONS
  1603. function getThisDuration(id) { let item_dur = Number(getData('#'+id +' .dirlist_item_media_duration','duration') ); if ( id !== undefined ) { return ( isNaN(item_dur)? Number.NaN : item_dur ); } } // return dur or NaN
  1604. function setThisDuration(id,dur) { getEl('#'+id).querySelector('.dirlist_item_media_duration').dataset.duration = dur; }
  1605. function setMediaDuration(id,kind,duration) { let el; // ===> SET MEDIA DURATION
  1606. if ( id === 'content_iframe_file' || /youtube.com|youtu.be/.test(getEl('#'+id+' a')?.href) ) { return; } // do not attempt to set duration for iframe files or youtube playlist items
  1607. try { el = getEl('#'+id); setThisDuration(id,duration);
  1608. switch(true) {
  1609. case Number(duration) === 0: case isNaN(Number(duration)): el.classList.add('disabled'); break; // if duration is NaN, disable and show spinner
  1610. default: el.querySelector('.dirlist_item_media_duration').innerText = getFormattedDuration(duration); el.classList.remove('disabled'); el.querySelector('input')?.removeAttribute('disabled');
  1611. statsSetTotalDuration(duration,kind); break; // if duration is a number, update stats
  1612. }
  1613. } catch (error) { null }
  1614. }
  1615. function statsSetTotalDuration(duration,kind) { let media_items = getEls('.dirlist_item.media'); if ( !media_items ) { return; } // if no media items, or total times already calculated, abort
  1616. let total_duration = Number(getEl('#total_duration')?.dataset.total_duration), audio_duration = 0, video_duration = 0;
  1617. switch(true) {
  1618. case kind !== 'refresh_all': addClass('body','has_'+ kind); // after opening subdir
  1619. total_duration = Number(total_duration) + Number(duration);
  1620. getEl('#total_duration').dataset.total_duration = total_duration; getEl('#total_duration').innerText = getFormattedDuration(total_duration); break; // set display total duration
  1621. default:
  1622. for ( let i = 0; i < media_items.length; i++ ) { duration = getThisDuration(media_items[i].id); // get duration from dirlist item
  1623. if ( !isNaN(Number(duration)) ) { total_duration = Number(total_duration) + Number(duration); } else { return } // update total duration
  1624. switch(true) { // update audio and video total durations
  1625. case kind === 'audio': audio_duration = Number(audio_duration) + Number(duration); // add has_audio class, increment total audio duration
  1626. setAttr('#stats_details_items span.audio','data-audio_duration',' (Total Time: '+ getFormattedDuration(audio_duration) +')'); break;
  1627. case kind === 'video': video_duration = Number(video_duration) + Number(duration); // add has_video class, increment total video duration
  1628. setAttr('#stats_details_items span.video','data-video_duration',' (Total Time: '+ getFormattedDuration(video_duration) +')'); break;
  1629. }
  1630. setAttr('#total_duration','data-total_duration',total_duration); getEl('#total_duration').innerText = getFormattedDuration(total_duration); // set display total duration
  1631. }
  1632. }
  1633. }
  1634. // REFRESH MEDIA DURATIONS
  1635. function refreshMediaDurations(id) { let media_items, item = getEl('#'+id), link = item?.querySelector('a')?.href, kind = item?.dataset?.kind; // ===> REFRESH MEDIA DURATIONS from menu or selecting [Error] media file
  1636. switch(true) {
  1637. case id === 'refresh_media_durations': // refresh all durations; from click refresh media durations menu item
  1638. media_items = Array.from(getEls('.dirlist_item.media')); media_items = media_items.filter( (el) => { return ( isNaN(getThisDuration(el.id))) }); // only update items if duration is not a number
  1639. media_items.forEach( el => refreshMediaDurations(el?.id) ); // send each media item with id back to function for default processing
  1640. if ( isTopWindow() ) { messageSend('iframe','refresh_media_durations'); } break; // send refresh message to iframe
  1641. default: if ( isNaN(getThisDuration(id)) ) { setThisDuration(id,0); getMediaDuration( link,kind,id ); } break; // refresh duration by id; first set duration to 0 to show loading spinner
  1642. }
  1643. }
  1644. //==============================//
  1645. function buildStats(stats_classes,stats_kinds,stats_total_size) { //*** BUILD STATS
  1646. stats_classes.sort();
  1647. let total_items = stats_classes.length, counts = {}, kinds = [], stats_items = [], total_dirs = 0, total_files = 0, total_dirs_invisible = 0, total_files_invisible = 0, total_invisibles = '';
  1648. for ( let i = 0; i < total_items; i++ ) { // Get counts
  1649. stats_classes[i] = stats_classes[i].split(' ').reverse().join(' ') // reorder classes to make invisible/ignored last
  1650. counts[stats_classes[i]] = 1 + ( counts[stats_classes[i]] || 0 ); // get key/value pairs for item_classes/total counts
  1651. switch(true) {
  1652. case ( !/invisible|ignored/.test(stats_classes[i]) ): break; // don't count :not(.invisible) and :not(.ignored)
  1653. case (getCurrentUIPref('show_invisibles') === 'true' ) && ( getCurrentUIPref('show_ignored_items') === 'false' ): // show_invisibles && hide_ignored
  1654. if ( /invisible/.test(stats_classes[i]) && /ignored/.test(stats_classes[i]) ) { break; } // don't count .invisible.ignored
  1655. if ( /dir/.test(stats_classes[i]) ) { total_dirs_invisible++; } else { total_files_invisible++; } break; // else count .ignored
  1656. case (getCurrentUIPref('show_invisibles') === 'false' ) && ( getCurrentUIPref('show_ignored_items') === 'false' ): // hide_invisibles && hide_ignored (hide all)
  1657. if ( /dir/.test(stats_classes[i]) ) { total_dirs_invisible++; } else { total_files_invisible++; } break; // count .invisible and .ignored (count all)
  1658. case (getCurrentUIPref('show_invisibles') === 'true' ) && ( getCurrentUIPref('show_ignored_items') === 'true' ): break; // don't count .invisible or .ignored (count none)
  1659. case (getCurrentUIPref('show_invisibles') === 'false' ) && ( getCurrentUIPref('show_ignored_items') === 'true' ): // hide_invisibles && show_ignored
  1660. if ( !/invisible/.test(stats_classes[i]) && /ignored/.test(stats_classes[i]) ) { break; } // don't count .ignored:not(.invisible)
  1661. if ( /dir/.test(stats_classes[i]) ) { total_dirs_invisible++; } else { total_files_invisible++; } break; // else count .invisible and .invisible.ignored
  1662. }
  1663. }
  1664. for ( let i = 0; i < stats_kinds.length; i++ ) { kinds[stats_kinds[i]] = 1 + ( kinds[stats_kinds[i]] || 0 ); } // get key/value pairs for item kinds/counts
  1665. total_dirs = ( kinds.dir || 0 ); total_files = ( total_items - total_dirs ); // total dirs && files count
  1666. if ( getCurrentUIPref('show_invisibles') === 'false' || getCurrentUIPref('show_ignored_items') === 'false' ) {
  1667. total_invisibles = ' (+'+ (total_dirs_invisible + total_files_invisible) +')';
  1668. total_items = total_items - (total_dirs_invisible + total_files_invisible);
  1669. total_dirs = total_dirs - total_dirs_invisible;
  1670. total_files = total_files - total_files_invisible;
  1671. }
  1672. for ( let count in counts ) { // make detail item for each kind of dirlist item --> doesn't preserve order
  1673. let kinds_items = count.split(' '), temp_items = [], stats_Item_Kinds = '';
  1674. kinds_items.forEach( item => ( !/ignored|invisible/.test(item) ? temp_items.unshift(item) : temp_items.push(item) ) ); kinds_items = temp_items;
  1675. kinds_items.forEach( item => ( stats_Item_Kinds += (`<span class="${ item }" >`+ (/ignored|invisible/.test(item) ? ' ('+item+')' : item.trim() ) +`</span>`)) );
  1676. let stats_item = `<li class="stats_list_item display_grid ${ kinds_items[0] }" data-kind="${ kinds_items[0] }"><a class="icon stats_list_item_name_a display_flex" data-count="${ counts[count] }"><span class="has_icon_before_before"></span><span class="stats_list_item_name_a_span">${ stats_Item_Kinds }</span></a></li>`;
  1677. stats_items.push(stats_item);
  1678. }
  1679. stats_items.sort();
  1680. return `<nav id="stats_container" class="display_flex width_100"><div id="stats" class="normal pointer overflow_hidden font_size_small"> <ol id="stats_summary" class="background_grey_80 text_color_default margin_0 padding_0"> <li class="stats_list_item line_height_1_2 no_highlight padding_4_8"><span id="stats_summary_totals" class="display_flex align_left" data-size="${ stats_total_size }">${ total_items } Items${ total_invisibles }: ${ total_dirs } Dirs, ${ total_files } Files (${ formatBytes(stats_total_size,2) })</span><span id="total_duration" class="display_none" data-total_duration=""></span></li> </ol> <ol id="stats_details_summary" class="border_bottom position_relative background_grey_80 text_color_default margin_0 padding_0 display_none"> <li id="stats_details_summary_total" class="summary_detailed border_bottom padding_4_8 no_highlight"><span>${ total_items } Items (${ total_dirs_invisible + total_files_invisible } invisible or ignored)</span></li> <li id="stats_details_summary_dirs" class="stats_list_item line_height_1_2 dir summary_detailed background_grey_85 padding_0"><a class="icon stats_list_item_name_a display_flex" data-count="${ total_dirs }"> <span class="stats_list_item_name_a_span display_flex has_icon_before stats_kind">Dirs (${ total_dirs_invisible } invisible or ignored)</span> </a></li> <li id="stats_details_summary_files" class="stats_list_item line_height_1_2 file summary_detailed background_grey_85 padding_0"><a class="icon stats_list_item_name_a display_flex" data-count="${ total_files }"><span class="stats_list_item_name_a_span display_flex has_icon_before stats_kind">Files (${ total_files_invisible } invisible or ignored)</span></a></li> </ol> <div id="stats_details_items_container"> <ol id="stats_details_items" class="margin_0 padding_0 position_relative display_none"> ${ stats_items.join('\n') } </ol> </div> </div></nav>`;
  1681. }
  1682. function updateStats(bool) { // ===> UPDATE STATS (bool: add or subtract size from total)
  1683. let items = getEls('.dirlist_item'), item_info, total_item_size, stats_classes = [], stats_kinds = [], item_classlist = [], total_size = 0; // get all dir_list items
  1684. getEls('.dirlist_item_details.size').forEach(el => total_size += Number(el.dataset.size));
  1685. total_item_size = ( bool === false ? total_size : Number(getData('#stats_summary_totals','size')) );
  1686. for ( let i = 0; i < items.length; i++ ) { // get classes and kind for each item
  1687. item_info = getLinkInfo( items[i].getElementsByClassName('dirlist_item_name_a')[0].href ); // get item info = [link,name,ext,kind,item_classes,body_classes];
  1688. item_classlist = item_info[4]; item_classlist = item_classlist.replace(/file|media|audio_loaded|content_loaded|has_subdirectory|selected|non_/g,'').trim(); // get item_classlist; remove unwanted classes
  1689. stats_classes.push(item_classlist); stats_kinds.push( item_info[3] ); // add item_classlist to stats_classes; add Item_Kinds to stats_kinds
  1690. total_item_size += Number(items[i].querySelector('.size').dataset.size);
  1691. }
  1692. getEl('#stats_container').remove(); getEl('#sidebar_footer').insertAdjacentHTML('afterbegin',buildStats(stats_classes,stats_kinds,total_item_size,2)); // remove old stats; build new stats and add to sidebar_footer
  1693. statsSetTotalDuration(null,'refresh_all'); initStatsEvents(); // initial event listeners for new stats items
  1694. }
  1695. // ***** END DIR_LIST SETUP ***** //
  1696. //============================//
  1697. // ***** UI SETUP ***** //
  1698. function prepDocHead(agent) { // ===> PREP DOC HEAD
  1699. document.title = 'Index of '+ window_location; // change the doc title to current location
  1700. for ( let attr_name of getEl('html').getAttributeNames() ) { getEl('html').removeAttribute(attr_name); } // remove html attributes, if any
  1701. getEl('head title').removeAttribute('id');
  1702. getEls('head meta, head base, head link, head style, head script, head noscript').forEach( headEl => headEl.remove() ); // remove various head elements
  1703. let head_content = '<meta charset="utf-8"><base href="'+ window.location.origin +'">' + getEl('head').innerHTML.replace(/<!--(?!>)[\S\s]*?-->/g,''); // add meta and remove conditional comments
  1704. if ( window.location.protocol.startsWith('file') ) { head_content = get_SVG_UI_File_Icon('favicon') + head_content; } // add custom favicon for local directories
  1705. getEl('head').innerHTML = head_content + addStyles(agent); // replace head content with prepped content
  1706. }
  1707. function getUIPrefBodyClasses(agent) { // ===> GET UI PREF BODY CLASSES and other initial settings
  1708. let queries = new URLSearchParams(window.location.search).entries(); queries = Object.fromEntries(queries); // make new search params from window.location.search
  1709. let body_classes = [], settings = Object.assign({},queries,UI_Settings); // merge UI_Settings and query settings
  1710. for ( let key in settings ) {
  1711. switch(true) {
  1712. case ['grid_font_size','grid_image_size','ui_font','ui_scale','show_image_thumbnails'].includes(key): break; // ignore these keys (values set in css or by buildTextEditorUI)
  1713. case ['sort_by','sort_direction','theme','texteditor_view'].includes(key): body_classes.push( key +'_'+ getCurrentUIPref(key) ); break; // other non-booleans: class = key + value
  1714. case getCurrentUIPref(key) === 'false': body_classes.push( key +'_false' ); break; // booleans: only add false values
  1715. }
  1716. }
  1717. body_classes.push(agent); body_classes.push('is_'+getOS()); // add browser and os classes
  1718. return body_classes.sort().join(' ');
  1719. }
  1720. function makeNewIndex(el,sort,agent,body_id) { // ===> MAKE NEW INDEX
  1721. const index_items = getIndexItems(agent), items = index_items[0], type = index_items[1];
  1722. const converted_index = convertIndexItems( items, type ); // = array of rows: ["link","date","size"]
  1723. switch(type) {
  1724. case 'error': return [[['<tr id="is_error"><td id="is_error_items" class="padding_6_8">'+ ( items === undefined ? '' : items ) +'</td></tr>'],'is_error'],'','',index_items];
  1725. default: return [buildNewIndex( el.id, converted_index, sort, type, body_id )];
  1726. }
  1727. }
  1728. // ===> BUILD IFRAME DIR LIST UI, with utility iframe for subdirectories add
  1729. function buildIframeUI(src,file_name,agent) {
  1730. let parent_link = src.split('/').slice(0,-2).join('/') + '/', query_str = new URLSearchParams(window.location.search.toString().slice(1));
  1731. let subdirectory = query_str.get('subdirectory') || null, body_id = query_str.get('body_id');
  1732. let iframe_directory, iframe_head, iframe_dir_list, content_body, gecko_styles, body_classes, iframe_utility_iframe, new_index, make_new_index, additional_classes;
  1733. window.onmessage = function(e) { messageReceive(e); return; } // init receive messages
  1734. switch(true) {
  1735. case window.location.search === "": // nobreak; case is true when opening dirs from sidebar source dir
  1736. case ( query_str.get('show_directory_source') || query_str.get('is_error') ) === 'true':
  1737. if ( elExists('#iframe_dir_styles') ) { getEl('#iframe_dir_styles').remove(); } break; // do nothing when viewing directory source or if error page...
  1738. default: // ...else set up iframe directory:
  1739. iframe_head = getEl('head'); content_body = getEl('body'); iframe_dir_list = ''; gecko_styles = ''; body_classes = [];
  1740. iframe_utility_iframe = '<iframe id="content_iframe_utility" sandbox="allow-scripts allow-same-origin allow-modals allow-popups" style="display:none;"></iframe>';
  1741. if ( /\.php\?/.test(src) ) { query_str = new URLSearchParams(makeSrcSearchParams('dir')); } // define default params for index.php?folder=... pages
  1742. for ( let key of query_str.keys() ) { // add various body_classes...
  1743. switch(true) {
  1744. case ( /show_details|ui_font/.test(key) ): break; // show details by default
  1745. case query_str.get(key) === 'true': break; // ignore true booleans
  1746. case query_str.get(key) === 'false': body_classes.push(key+'_false'); break; // add body classes for false boolean params
  1747. default: body_classes.push(key+'_'+getCurrentUIPref(key)); break; // non-boolean params (theme, sort)
  1748. }
  1749. }
  1750. if ( agent === 'is_gecko' ) { gecko_styles = ('<style id="gecko_style_rules">'+ gecko_style_rules +'</style>'); }
  1751. new_index = makeNewIndex(content_body, query_str.get('sort_by'),'',body_id); // make new index
  1752. make_new_index = new_index[0];
  1753. additional_classes = (new_index[0][1].trim().split(/\s+/)).concat(body_classes); // define additional body classes
  1754. if ( !/is_error/.test(new_index[0][1]) ) { // if not an a error page...build the ui
  1755. iframe_head.querySelectorAll('style,script,meta,link[rel="stylesheet"],link[href$="css"]').forEach( el => el.remove() ); // remove any existing directory index styles
  1756. iframe_head.insertAdjacentHTML('beforeend',`<style id="iframe_dir_styles">${iframe_dir_styles }</style><style id="sidebar_styles">${ sidebar_styles }</style><style>${ content_pane_styles }</style><style id="font_styles"></style><style id="font_grid_styles"></style>${gecko_styles}`); // assemble the iframe head
  1757. switch(true) { // Assemble content_iframe and utility_iframe content
  1758. case subdirectory === 'true': iframe_dir_list = `<div id="directory_list_outer"><ol id="directory_list" class="border_bottom text_color_default">${ make_new_index[0] }</ol></div>`; break;
  1759. default:
  1760. iframe_directory = Sidebar_Elements('iframe',parent_link); // create iframe directory elements
  1761. iframe_dir_list = iframe_directory.replace(/insert_prepped_index/,make_new_index[0]).replace(/insert_stats/,make_new_index[2]); // assemble iframe directory
  1762. content_body.removeAttribute('style'); content_body.style.fontFamily = getCurrentUIPref('ui_font'); // remove any body inline styles; set ui_font
  1763. content_body.classList.add(...additional_classes); // add body styles
  1764. }
  1765. content_body.innerHTML = iframe_dir_list + Content_Pane_Elements(); // append iframe_dir_list and content_pane for quicklook
  1766. }
  1767. if ( subdirectory === null ) { content_body.insertAdjacentHTML('beforeend',iframe_utility_iframe); initIframeEvents(); } // don't multiply utility_iframes; init iframe event listeners
  1768. if ( subdirectory === 'true' ) {
  1769. messageSend('top_body','dirlist_subdir_loaded','',[getEl('#directory_list').innerHTML,make_new_index[1],getCurrentUIPref('parent_id')] ); // send prepped subdir to parent window
  1770. } else {
  1771. messageSend('top_body','iframe_loaded','',[src,file_name,'dir']); // else send iframe_loaded message
  1772. }
  1773. }
  1774. }
  1775. // ===> BUILD TEXT EDITOR UI
  1776. function buildTextEditorUI(kind) { let raw_markdown, body_classes = [], content;
  1777. if ( !hasClass('body','has_texteditorUI') ) { // add classes, styles, and scripts; only add once
  1778. getEl('head').insertAdjacentHTML('beforeend','<style id="texteditor_styles">'+ texteditor_styles +'</style>');
  1779. getEl('head').insertAdjacentHTML('beforeend','<link id="github_markdown_css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.min.css"></link>');
  1780. body_classes.push('has_texteditorUI', 'texteditor_view_'+getCurrentUIPref('texteditor_view'));
  1781. }
  1782. switch(true) { // get source text and append UI elements
  1783. case !isTopWindow(): // iframe text editing UI
  1784. window.onmessage = function(e) { messageReceive(e); return false; } // init receive messages is_link
  1785. getEl('head').insertAdjacentHTML('afterbegin','<meta charset="utf-8" /><meta http-equiv="Content-Type" content="text/plain; charset="utf-8">');
  1786. getEl('head').insertAdjacentHTML('beforeend','<style id="global_styles">'+ global_styles +'</style><style id="utilities_styles">'+ utilities_styles +'</style>'); // add iframe text editing styles
  1787. if ( kind === 'link' ) { tempHideTexteditor(); } // prevent FOUC for some kinds of content...
  1788. raw_markdown = decodeURIComponentSafe( getEl('body').innerText ); // get source text and decode Unicode chars.
  1789. document.body.innerHTML = Content_Pane_Elements('content_text_elements') + Utilities_Elements('texteditor'); // add the UI
  1790. getEl('#texteditor_raw_pane').value = raw_markdown; // set the source text value
  1791. getEl('#texteditor_raw_pane').setSelectionRange(0,0); // set the insertion point to the beginning of the text
  1792. if ( /^\#EXTM3U/m.test(getEl('#texteditor_raw_pane').value) ) { content = getEl('#texteditor_raw_pane').value.trim(); messageSend('top_body','iframe_playlist','',content); } // playlists & filelists
  1793. addClass('body','is_text'); searchParamsGet().forEach((key, value) => { addClass('body',value+"_"+key); }); // add text editor body classes
  1794. break;
  1795. }
  1796. switch(true) { // assemble text editing body classes
  1797. case getCurrentUIPref('text_editing_enable') === 'false' && !isTopWindow(): // if text editing disabled...
  1798. if ( hasClass('body','texteditor_view_html') ) { searchParamSet('texteditor_view','raw'); }
  1799. removeClass('body','texteditor_view_html texteditor_split_view_true'); // remove split_view and view_html classes
  1800. body_classes.push('text_editing_enable_false','texteditor_split_view_false','texteditor_view_'+getCurrentUIPref('texteditor_view'));// show the raw text, no split view
  1801. setAttr('#texteditor_raw_pane','readonly',''); break; // disable textarea editing
  1802. default: // ...otherwise set up text editing
  1803. textEditorTogglePrefs('texteditor_split_view', ( getCurrentUIPref('texteditor_split_view') === 'false' ? 'false' : 'true' )); // set split view
  1804. textEditorTogglePrefs('texteditor_sync_scroll',( getCurrentUIPref('texteditor_sync_scroll') === 'false' ? 'false' : 'true' )); // set sync scroll
  1805. }
  1806. TextEditing(); initTextEditorEvents(); // text editing functions & init text editor event listeners
  1807. }
  1808. // ===> BUILD UI: Append all assembled elements to body
  1809. function buildUI() { let make_new_index, body_classes, main_content, agent = getBrowser(), link_info, file_name, kind, iframe_src;
  1810. switch(true) {
  1811. case isTopWindow(): // if it's not an iframe...
  1812. make_new_index = makeNewIndex('body','',agent,'top_body'); // make index
  1813. if ( make_new_index[0] === undefined ) { return; } // in case user cancels processing of large directory > 5000 items
  1814. body_classes = make_new_index[0][1] +' '+ getUIPrefBodyClasses(agent); // delete extra spaces, create array of body class names
  1815. main_content = `${ Sidebar_Elements('top_body') } ${ Content_Pane_Elements() } ${ Utilities_Elements('top_body') }`; // assemble html elements
  1816. if ( make_new_index[0][1] !== 'is_error' ) {
  1817. main_content = main_content.replace(/insert_prepped_index/,make_new_index[0][0]).replace(/insert_stats/,make_new_index[0][2]); // build dir_index and stats, add to MainContent
  1818. } else {
  1819. main_content = main_content.replace(/insert_prepped_index/,'').replace(/<ul id=\"sidebar_header_utilities_row_1[\s\S]+Show Font Grid<\/li>\s*<\/ul>\s*<\/li>\s*<\/ul>/m,'').replace(/<ul id=\"show_texteditor[\s\S]+Text Editor<\/span><\/li><\/ul>/,'').replace(/<footer[\s\S]+footer>/,''); // build error ui
  1820. }
  1821. document.body.innerHTML = ''; // remove body contents
  1822. prepDocHead(agent); // add title, favicon, meta tags, styles to head
  1823. setAttr('body','id','top_body'); // add body id
  1824. setAttr('body','lang','en'); // add body lang attr
  1825. if ( document.body.getAttribute('class') !== null ) { document.body.removeAttribute('class'); } // remove body classes, if any
  1826. addClass('body',body_classes); // add body classes
  1827. document.body.innerHTML = main_content; // add main content to body
  1828. uiPrefFontSet(); uiPrefScaleSet(null,Number(getCurrentUIPref('ui_scale')),true); // set ui font and scale
  1829. if ( make_new_index[0][1] !== 'is_error' ) { initEvents(); autoLoadItems(); }
  1830. else { initBaseEvents(); showDirectorySource(window.location.origin + window.location.pathname + '?error=is_error'); addClass('#content_iframe','has_content'); } break;
  1831. case !isTopWindow() && !window.location.pathname.endsWith('.pdf'): // if iframe and not pdf (i.e. embed el), setup iframe UI
  1832. iframe_src = window.location.href; link_info = getLinkInfo(iframe_src); file_name = link_info[1]; kind = link_info[3];
  1833. setAttr('body','id','content_body'); // add iframe body id
  1834. switch(true) { // determine UI type...
  1835. case ( /\.php\?/.test(iframe_src) ): // attempt to deal with some .php?folder links
  1836. case ( /app|dir/.test(kind) && !/is_error/.test(iframe_src)): buildIframeUI(iframe_src,file_name,agent); addClass('body','is_dir'); break; // if dir, set up iframe dir list UI
  1837. case kind === 'link': case link_info[2] === 'cuetxt': main_content = document.body.innerText; // nobreak; if link or cuetxt, send file content to top
  1838. case ( /code|text|markdown|other/.test(kind) ) && !(window.parent !== window.top): buildTextEditorUI(kind); addClass('body','is_text'); // if text file, set up iframe text editor
  1839. main_content = getEl('#texteditor_raw_pane').value;
  1840. break; // get text content for optional processing (cuetxt)
  1841. case kind === 'htm': main_content = getEl('html').outerHTML; addClass('body','is_html'); // nobreak; if html file, get innerHTML
  1842. default: initIframeEvents(); break; // if any other iframe content
  1843. }
  1844. messageSend('top_body','iframe_loaded','',[iframe_src,file_name,kind,null,main_content]); // send iframe_loaded message (not dir|link file) with args
  1845. break;
  1846. }
  1847. uiPrefImgThumbsToggle(getCurrentUIPref('show_image_thumbnails')); // load image thumbnails after building ui
  1848. }
  1849. buildUI();
  1850. //============================//
  1851. // INITIALIZE EVENT LISTENERS
  1852. function initEvents() { if ( window.parent !== window.top ) { return; }
  1853. initBaseEvents(); initDirListEvents(); initStatsEvents(); initWarningEvents(); // ===> INIT EVENT LISTENERS // init base events, stats events, and dir list events
  1854. getEl('#default_settings').onclick = function(e) { uiPrefsReset(e); }; // settings to default
  1855. getEl('#show_directory_source').onclick = function(e) { e.stopPropagation(); showWarning('showDirectorySource'); }; // toggle show directory source
  1856. getEl('#open_in_content_pane').onclick = function(e) { e.stopPropagation(); showWarning('openSidebarInContentPane'); }; // open sidebar in content pane
  1857. getEl('#open_in_texteditor').onclick = function(e) { e.preventDefault(); openInTextEditor(); }; // openInTextEditor
  1858. getEls('#open_font_label, #open_playlist_label, textarea, div[contenteditable], select, #scale').forEach( el => el.onclick = function(e) { e.stopPropagation(); }); // stopProp on various clicks
  1859. getEls('#show_font_grid, #show_grid_btn, #show_image_grid, #show_texteditor, #texteditor').forEach( el => el.onclick = function(e) { eStopPrevent(e); showThis( el.id,false,true ); }); // show grids or text editor
  1860. getEl('#show_grid_btn').onmouseenter = function() { addClass('body','has_menu_grid'); }; getEl('#show_grid_btn').onmouseleave = function() { removeClass('body','has_menu_grid'); };
  1861. getEl('#close_audio').onclick = function(e) { e.stopPropagation(); showMedia('close_audio'); }; // close audio button click
  1862. getEl('#sidebar_footer').onclick = function() { removeClass('.dirlist_item','hovered'); }; //
  1863. getEl('#content_image').onclick = function(e) { e.stopPropagation(); scaleImages(e); }; // Zoom image on click
  1864. getEls('#content_title span').forEach( el => el.onclick = function(e) { e.stopPropagation(); textEditorTogglePrefs('text_editing_enable'); });
  1865. getEls('#content_title,#content_audio_title').forEach( el => el.onclick = function() { playlistShowItem(el.id); });
  1866. getEls('#open_font,#open_playlist').forEach( el => el.onclick = function(e) { openFiles(e,el.id); }); // Open files (onclick show warnings)
  1867. getEls('#open_font,#open_playlist').forEach( el => el.onchange = function(e) { e.preventDefault(); e.stopPropagation(); openFiles(e,el.id); }); // Open font and playlist files
  1868. getEl('#go_to_item').onclick = function(e) { goToItem(e); }; // Go to item
  1869. getEl('#show_invisible_items').onclick = function() { getEl('#show_invisibles').click(); getEl('#show_invisibles').blur(); } //
  1870. getEl('#play_toggle').onclick = function(e) { toggleAllChecked(e); }; // toggle media checkboxes
  1871. getEl('#make_playlist').onclick = function(e) { eStopPrevent(e); showWarning('warning_make_playlist'); }; // make playlist
  1872. getEl('#save_svg').onclick = function(e) { e.stopPropagation(); fontGlyphSave(); }; // save glyph as svg
  1873. getEl('#sidebar_footer_utilities').onmouseenter = function() { addClass('body:not(.has_menu), body:not(.has_menu_parents)','has_menu_footer'); };
  1874. getEl('#sidebar_footer_utilities').onmouseleave = function() { removeClass('body:not(.has_menu), body:not(.has_menu_parents)','has_menu_footer'); };
  1875. getEls('.prev_next_btn').forEach( el => el.onclick = function(e) { contentHeaderButtons(e,el.id); }); // ============> combine with next?
  1876. getEls('#increase,#decrease').forEach( el => el.onmousedown = function(e) { e.preventDefault(); contentHeaderButtons(e,this.id); }); // onclick scale buttons
  1877. getEls('#audio,#content_video').forEach( el => el.onended = function() { navigateByArrowKey(['ArrowRight',true]); }); // autoplay media onended
  1878. getEls('#loop_media_files,#shuffle_media_files,#audio_options input,#audio_options label').forEach( el => el.onclick = function(e) { el.blur(); mediaPlaybackOptions(e,el.id); }); // media loop/shuffle options
  1879. getEl('#refresh_media_durations').onclick = function() { refreshMediaDurations('refresh_media_durations'); };
  1880. getEls('#parent_dir_nav #svg_multiply,#close_playlist').forEach(el => el.onclick = function(e) { e.preventDefault(); e.stopPropagation(); showWarning('closePlaylist','close_playlist'); });
  1881. getEl('#text_editing_enable').onclick = function() { textEditorTogglePrefs('text_editing_enable'); menuClose(); };
  1882. // getEl('#audio').ontimeupdate = getMediaTimeRemaining; // update remaining media time; !!!disabled until remaining time fixed
  1883. }
  1884. function initBaseEvents() { // ===> INIT BASE EVENT LISTENERS: minimal listeners needed for error pages
  1885. window.onmessage = function(e) { messageReceive(e); return false; } // init receive messages
  1886. document.body.onclick = function() { menuClose(); }; // close menu click
  1887. getEls('.menu_container').forEach( el => el.onclick = function(e) { e.stopPropagation(); menuShow(e,el.id); }); // toggle menus on click
  1888. getEls('#sidebar_menu_main li').forEach( el => el.onclick = function(e) { e.stopPropagation(); menuClick(); menuClose(e); });
  1889. getEls('#sidebar_menu_main li').forEach( el => el.onmouseenter = function() { removeClass('#sidebar_menu_main li','selected hovered'); addClass('#sidebar_menu_main li:hover','selected'); });
  1890. getEls('.toggle_UI_pref').forEach( el => el.onclick = function(e) { uiPrefToggleOnClick(e,this.id); menuClose(e); }); // toggle UI prefs click
  1891. getEl('#font_toolbar').onclick = function(e) { e.stopPropagation(); }
  1892. getEls('#sidebar,#content_header').forEach( el => el.onclick = function(e) { e.stopPropagation(); focusEl('#top_body'); }); // focus sidebar on click
  1893. getEls('#content_pane, #content_pane .content_el').forEach( el => el.onclick = function(e) { e.stopPropagation(); focusEl('#'+ el.id); }); // focus content on click
  1894. getEl('#ui_font').onclick = function(e) { uiPrefFontSet(e,this.id) }; // show the UI font textarea
  1895. getEl('#ui_scale input').oninput = function(e) { e.stopPropagation(); setData('#ui_scale span.menu_item','value',Math.round(this.value)+'%'); }; // show scale %
  1896. getEl('#ui_scale input').onmouseup = function(e) { uiPrefScaleSet(e,this.value); }; // scale UI
  1897. getEl('#ui_scale').ondblclick = function(e) { uiPrefScaleSet(e,100) }; // set 100% scale on dblclick
  1898. getEl('#show_help').onclick = function(e) { e.preventDefault(); addClass('#top_body','has_help'); }; // show help click
  1899. getEl('#close_help').onclick = function(e) { e.preventDefault(); removeClass('body','has_help'); }; // close help click
  1900. getEl('#help_container').onclick = function(e) { e.stopPropagation(); }; // help container: ignore clicks
  1901. getEl('#close_btn').onclick = function(e) { eStopPrevent(e); closeContent(); this.blur(); }; // close button
  1902. getEl('#reload_btn').onclick = function(e) { eStopPrevent(e); resetContent(); this.blur(); this.classList.remove('reset'); }; // reset btn
  1903. getEl('#handle').onmousedown = function(e) { resizeSidebar(e); } // resize sidebar
  1904. document.addEventListener('mouseup',function() { document.onmousemove = null; }); // revoke drag on mouseup
  1905. document.addEventListener('mousemove',function() { removeClass('body','no_hover'); }); // remove no_hover class
  1906. }
  1907. function initWarningEvents() {
  1908. getEls('#warnings_container button')?.forEach( el => el.onclick = function(e) { eStopPrevent(e); warningButtons( el.id ); });
  1909. getEls('body.has_overlay, body.has_warning').forEach( el => el.onclick = function(e) { eStopPrevent(e); return; }); // prevent user actions with warning or overlay
  1910. getEls('body.has_overlay, body.has_warning').forEach( el => el.onmousedown = function(e) { eStopPrevent(e); return; }); // prevent user actions with warning or overlay
  1911. getEls('body.has_overlay, body.has_warning').forEach( el => el.onmouseup = function(e) { eStopPrevent(e); return; }); // prevent user actions with warning or overlay
  1912. }
  1913. function initDirListEvents() { if ( !isTopWindow() ) { return; } // ===> INIT DIR_LIST EVENT LISTENERS; called whenever new dir list items added
  1914. getEls('.dirlist_item')?.forEach( el => el.onclick = function(e) { clickDirListItem(e,el.id); }); // show item or play/pause media
  1915. getEls('.dirlist_item.dir .has_icon_before_before')?.forEach( el => el.onclick = function(e) { subDirOpenClose(e,el.closest('.dirlist_item').id); }); // open/close subdirectories
  1916. getEls('.dirlist_item.dir')?.forEach( el => el.ondblclick = function(e) { e.preventDefault(); showWarning('dirOpen', [el.id, el.querySelector('a').href] ); });
  1917. getEls('.dirlist_item.link')?.forEach( el => el.ondblclick = function(e) { openLinkFile(e,el.id); }); // open link files on dblclick
  1918. getEls('.dirlist_item.media input')?.forEach( el => el.onmousedown = function(e) { toggleChecked(e,el.closest('.dirlist_item').id); }); // toggle media checkboxes
  1919. getEls('.dirlist_item.media input')?.forEach( el => el.onclick = function(e) { el.blur(); e.preventDefault(); e.stopPropagation(); }); // Click media checkboxes
  1920. getEls('.dirlist_item.playlist')?.forEach( el => el.ondblclick = function(e) { eStopPrevent(e); clickThis('#open_playlist'); }); // open playlist
  1921. getEls('.dirlist_item.non_local')?.forEach( el => el.onmouseenter = function() { el.title = 'Non-local file'; }); // add non-local title prop
  1922. getEl('#show_invisibles_container')?.addEventListener('click',function(e) { e.stopPropagation(); clickThis('#show_invisibles_container input'); });
  1923. }
  1924. function initStatsEvents() { // ===> INIT STATS EVENT LISTENERS
  1925. getEl('#stats')?.addEventListener('mouseleave',function() { menuClose(); });
  1926. getEl('#stats_summary')?.addEventListener('click',function(e) { e.stopPropagation(); menuShow(e,'stats_summary'); }); // show stats
  1927. getEls('#stats_details_items li, #stats_details_summary_dirs, #stats_details_summary_files')?.forEach( el => el.onmouseenter = function() {
  1928. getEls('.dirlist_item'+ statsGetHoveredListClass(el))?.forEach( el => el.classList.add('hovered')); // add the hovered class
  1929. getEl('.dirlist_item.hovered')?.scrollIntoView({ behavior:'smooth',block:'nearest',inline:'nearest' }); // scroll 1st matched el
  1930. });
  1931. getEl('#stats_details_summary_total')?.addEventListener('click',function() { menuClose(); });
  1932. getEls('#stats_details_items li, #stats_details_summary_dirs, #stats_details_summary_files')?.forEach( el => el.onmouseleave = function() { removeClass('.dirlist_item.hovered','hovered'); });
  1933. getEls('#stats_details_items li, #stats_details_summary_dirs, #stats_details_summary_files')?.forEach( el => el.onclick = function() { // onclick stats footer detail items
  1934. if ( !hasClass('body','sort_by_kind') ) { clickThis('#sort_by_kind'); } // sort by kind
  1935. if ( getEl('.dirlist_item'+ statsGetHoveredListClass(el)) !== null ) {
  1936. if ( el.classList.contains('invisible') && getCurrentUIPref('show_invisibles') === 'false' ) { clickThis('#show_invisibles'); }
  1937. if ( el.classList.contains('ignored') && getCurrentUIPref('show_ignored_items') === 'true' ) { clickThis('#show_ignored_items'); }
  1938. if ( isTopWindow() ) { showThis( getEl('.dirlist_item'+ statsGetHoveredListClass(el)).id ); } else { showThis(getEl('.dirlist_item'+ statsGetHoveredListClass(el)).id,true,false); } // click first matched item
  1939. }
  1940. });
  1941. }
  1942. function initGridItemEvents() { // ===> INIT GRID ITEM EVENT LISTENERS
  1943. getEls('#content_grid .grid_item')?.forEach( el => el.onclick = function(e) { e.preventDefault(); e.stopPropagation(); showContentGridItem(e,el.dataset.id,el.querySelector('a').href,el.dataset.kind); }); // grid item
  1944. getEls('#content_grid .grid_item:not(.selected)')?.forEach( el => el.onmouseenter = function() { addClass('#'+el.dataset.id,'hovered'); scrollThis('#directory_list','.hovered'); });
  1945. getEls('#content_grid .grid_item:not(.selected)')?.forEach( el => el.onmouseleave = function() { removeClass('#'+el.dataset.id,'hovered'); });
  1946. getEls('.dirlist_item.image,.dirlist_item.font')?.forEach( el => el.onmouseenter = function() {
  1947. if ( hasContent('grid') ) { addClass('#content_grid > .grid_item[data-id="'+ el.id +'"]','hovered'); scrollThis('#content_grid','.hovered'); } });
  1948. getEls('.dirlist_item.image,.dirlist_item.font')?.forEach( el => el.onmouseleave = function() {
  1949. if ( hasContent('grid') ) { removeClass('#content_grid > .grid_item[data-id="'+ el.id +'"]','hovered'); scrollThis('#content_grid','.hovered'); } });
  1950. }
  1951. function initCuesheetEvents() { // ===> INIT CUESHEET EVENT LISTENERS
  1952. getEls('.cuesheet_track_list_container li')?.forEach( el => el.onclick = function(e) { menuShow(e,el.id); });
  1953. getEls('.cuesheet_track_list_container')?.forEach( el => el.onmouseenter = function(e) { // add selected class to first track whose time is less than currentTime
  1954. menuShow(e,this.id)
  1955. if ( el.id === 'cuesheet_track_list_container_video' ) { getEl('#cuesheet_track_list_video').style.top = getEl('#content_title_container').clientHeight; }
  1956. let currentTime = getEls('#audio,#content_video')?.currentTime;
  1957. el.querySelector('.cuesheet_track_list').setAttribute('data-duration',currentTime); // set duration attr of cue sheet track list
  1958. this.querySelector('.cuesheet_track_list').style.height = getEl('#content_container').height + getEl('#content_title_container').height; // set height of cue sheet track list
  1959. let currentTrack = Array.from(el?.querySelector('.cuesheet_track_list li')).reverse().filter(function() { return parseInt( el?.dataset?.duration ) <= Math.round(currentTime); })[0];
  1960. currentTrack?.parentElement?.children?.classList.remove('selected');
  1961. currentTrack?.classList.add('selected');
  1962. });
  1963. }
  1964. //============================//
  1965. // INITIALIZE IFRAME EVENT LISTENERS
  1966. function initIframeEvents() { initSubframeEvents(); if ( isTopWindow() || window.parent !== window.top ) { return; } // ===> INIT IFRAME EVENT LISTENERS
  1967. document.body.onclick = function(e) { e.preventDefault(); focusEl('#content_iframe'); } // focus iframe
  1968. document.body.querySelectorAll('body,textarea,form,select,input,option,#sidebar')?.forEach( el => el.onclick = function(e) { e.stopPropagation(); messageSend('top_body','blur_top'); el.focus(); }); // focus iframe
  1969. if ( /(\.html*|\.php)$/.test(window.location.href) ) {
  1970. getEls('a')?.forEach( el => el.onclick = function(e) { iframeClick(e,'','link',el.getAttribute('href')); }); return; // return if html; rest unneeded
  1971. }
  1972. document.addEventListener('mousemove',function() { removeClass('body','no_hover'); }); // remove no_hover class
  1973. getEls('.dirlist_item')?.forEach( el => el.onclick = function() { showThis(el.id,true,false); }); // select clicked iframe dirlist item
  1974. getEls('.dirlist_item a')?.forEach( el => el.onclick = function(e) { e.preventDefault(); }); // do nothing for iframe dirlist links
  1975. getEls('.dirlist_item.dir .has_icon_before_before')?.forEach( el => el.onclick = function(e) { subDirOpenClose(e,el.closest('.dirlist_item').id); }); // open/close subdirs
  1976. getEls('.dirlist_item:not(.ignored)')?.forEach( el => el.ondblclick = function(e) { iframeClick( e,el.id,'link',el.querySelector('a').href ); }); // dblclick open iframe dirs/files
  1977. getEls('ul,li')?.forEach( el => el.onmousedown = function() { messageSend('top_body','menu_close'); });
  1978. getEl('#open_in_sidebar a')?.addEventListener('click',function(e) { e.preventDefault(); messageSend('top_body','open_iframe_dir_in_sidebar','',window.location.href); }); // no break;
  1979. getEl('#iframe_parent_link')?.addEventListener('click',function(e) { iframeClick(e,'iframe_parent_link','link',this.href); }); // iframe parent
  1980. getEls('.toggle_UI_pref')?.forEach( el => el.onclick = function(e) { uiPrefToggleOnClick(e,el.id); }); // toggle UI prefs
  1981. initStatsEvents(); // initialize stats events listeners
  1982. }
  1983. function initSubframeEvents() { if ( window.parent !== window.top ) { getEl('#content_body').addEventListener('click',function(e) { e.preventDefault(); e.stopPropagation(); }); } } // prevent events in quicklook
  1984. function initTextEditorEvents() { let preview = getEl('#texteditor_styled_pane'); // ===> INIT TEXT EDITOR EVENT LISTENERS
  1985. getEls('#content_texteditor, #content_texteditor *').forEach( el => el.onclick = function() { messageSend('top_body','menu_close'); });
  1986. getEls('#toolbar_buttons .toggle_UI_pref').forEach( el => el.onclick = function(e) { uiPrefToggleOnClick(e,el.id); }); // text editing UI is not in DOM on page load;
  1987. getEl('#texteditor_toolbar').onmousedown = function(e) { e.preventDefault(); }; // prevent textarea from losing focus if sidebar clicked
  1988. window.onresize = function() { texteditor_ResetSplit(); }; // reset text editor split
  1989. getEl('#text_editing_handle').ondblclick = function(e) { e.stopPropagation(); texteditor_ResetSplit(); }; // reset text editor split
  1990. getEl('#text_editing_handle').onmousedown = function(e) { eStopPrevent(e); texteditor_ResizeSplit(); }; // resize text editor panes
  1991. getEl('#text_editing_handle').onmouseup = function() { document.onmousemove = null; } // remove onmousemove
  1992. getEls('.checkbox_container').forEach( el => el.onclick = function(e) { toggleCheckBox(e,this.id); }); // toggle checkboxes (texteditor_preview, toolbar)
  1993. getEl('#texteditor_raw_pane').oninput = function() { // add edited body class; if iframe, send edited message; update live markdown preview
  1994. if ( !hasClass('body','texteditor_edited') ) { addClass('body','texteditor_edited'); if ( !isTopWindow() ) { messageSend('top_body','iframe_edited','',''); } } MDlivePreview();
  1995. }; messageSend('top_body','menu_close');
  1996. getEls('.texteditor_pane').forEach( el => el.onscroll = function(e) { texteditor_SyncScroll(e,this.id); });
  1997. getEls('#save_btn li').forEach( el => el.onclick = function() { texteditorSaveBtn(el.id); }); // save text editor content
  1998. getEl('#clear_text').onclick = function() { showWarning('texteditorClear'); }; // clear text button
  1999. preview.querySelectorAll('.checklist input').forEach( el => el.onclick = function(e) { e.stopPropagation(); MDliveCheckBoxes(el); }); // Live checkboxes
  2000. preview.querySelectorAll('.table-of-contents a').forEach( el => el.onclick = function(e) { e.preventDefault(); MDtocClick(el.id); }); // Preview TOC click navigation
  2001. preview.querySelectorAll('.uplink').forEach( el => el.onclick = function(e) { e.stopPropagation(); MDheaderClick(); }); // Click header uplinks
  2002. initWarningEvents();
  2003. }
  2004. function initFontPreviewEvents() { // ==> INIT FONT PREVIEW_EVENTS
  2005. getEls('#font_toolbar select,#font_toolbar textarea,#font_toolbar input').forEach( el => el.onclick = function(e) { e.stopPropagation(); el.focus(); });
  2006. getEls('#font_toolbar label').forEach( el => el.onclick = function(e) { e.stopPropagation(); el.parentElement.querySelector('input').focus(); }); // Stop click propagation
  2007. getEls('#content_font *').forEach( el => el.onmousedown = function(e) { e.stopPropagation(); focusEl(el.id); }); // Stop click propagation
  2008. getEls('#font_toolbar select').forEach( el => el.onchange = function(e) { fontOptions(e,el.id,el.value,el.options[el.selectedIndex].dataset?.prop,el.options[el.selectedIndex].dataset?.value) });
  2009. getEls('#font_toolbar textarea').forEach( el => el.onkeydown = function(e) { if ( /enter/.test(e.key.toLowerCase()) ) { e.preventDefault(); } }); // prevent typing return in textareas
  2010. getEls('#font_toolbar textarea,#font_toolbar input').forEach( el => el.oninput = function(e) { fontOptions(e,el.id,el.value) }); // init font toolbar specimen modifications
  2011. getEls('#font_specimen_adjustments li').forEach( el => el.ondblclick = function(e) { e.stopPropagation(); fontReset(el.dataset.inputid); }); // reset adjustments
  2012. getEls('.font_glyph_item').forEach( el => el.onclick = (e) => { showFontGlyph(e,el.id) }); // show font specimen glyph on click
  2013. getEl('#font_specimen_glyph').onmousedown = (e) => { fontGlyphMove(e,'#font_specimen_glyph'); } // init move glyph
  2014. }
  2015. //============================//
  2016. // INITIALIZE KEYDOWN EVENTS
  2017. function eKey_BackSlash(e) { // "BACKSLASH" KEY
  2018. switch(true) {
  2019. case cmdShiftKey(e): // Cmd Shift + \ : toggle split
  2020. switch(true) {
  2021. case isTopWindow() && hasContent('text,code,markdown'): messageSend('iframe','texteditor_split_view'); break; // send toggle split view message
  2022. case getEl('#texteditor_split_view').height > 0: getEl('#texteditor_split_view').click(); break; // if split view visible...click toggle split
  2023. } break;
  2024. case cmdKey(e): if ( !isTopWindow() ) { messageSend('top_body','show_sidebar'); } else { getEl('#show_sidebar').click(); } break; // Cmd + \ : toggle sidebar
  2025. }
  2026. }
  2027. function eKey_Enter(e) { // "ENTER" KEY
  2028. let selected_el = (hasContent('font_file') || hasContent('font_specimen') || hasContent('grid') ? getEls('#content_font .selected,#content_grid .selected')[0] : getEls('.dirlist_item.selected')[0]);
  2029. switch(true) {
  2030. case hasClass('body','has_menu'): e.preventDefault(); menuClick(); menuClose(); messageSend('iframe','menu_close'); break; // click selected menu item
  2031. case hasClass('body','has_warning') || hasClass('body','has_help'): e.preventDefault(); clickThis('button.focus, button:focus'); break; // click focused warning btn
  2032. case hasClass('body','focus_content') && hasContent('font') && selected_el !== null: showFontGlyph(e,selected_el.id); break; // show font glyph
  2033. case !isTopWindow(): // if iframe...
  2034. switch(true) {
  2035. case hasClass('body','has_top_menu'): messageSend('top_body','menuClick'); break; // close main menu
  2036. case elExists('.dirlist_item.audio.selected') && !hasClass('.dirlist_item.audio.selected','audio_loaded'):
  2037. iframeClick(e,getEl('.dirlist_item.selected').id,'dblclick',getEl('.dirlist_item.selected a').href); break;
  2038. case elExists('.dirlist_item.selected') && cmdKey(e): iframeClick(e,getEl('.dirlist_item.selected').id,'dblclick',getEl('.dirlist_item.selected a').href); break; // webloc or url file
  2039. case elExists('.dirlist_item.audio_loaded') && !hasClass('.dirlist_item.selected','audio_loaded'): eStopPrevent(e); mediaPlayPause(); break; // play/pause media
  2040. } break;
  2041. case selected_el?.classList.contains('app') && UI_Prefs_Bool.apps_as_dirs === false: break; // don't open app folders
  2042. default:
  2043. switch(true) {
  2044. case selected_el?.classList.contains('.disabled'): case hasContent('texteditor'): break; // no nothing for disabled or default behavior
  2045. case selected_el?.classList.contains('audio') && !selected_el.classList.contains('audio_loaded'): showMedia('audio',getEl('.dirlist_item.audio.selected').id ); break; // show selected audio file
  2046. case selected_el?.classList.contains('media'): eStopPrevent(e); mediaPlayPause(); break; // else play/pause media
  2047. case ( /dir|link|playlist/.test(selected_el?.classList) ) && cmdKey(e): dirOpen(e,getEl('.dirlist_item.selected').id,getEl('.dirlist_item.selected a').href); break; // open dirs, links, playlists
  2048. case ( /dir|link/.test(selected_el?.classList) ): // nobreak
  2049. default: e.stopPropagation(); selected_el?.click(); // default: click selected item
  2050. }
  2051. }
  2052. }
  2053. function eKey_Escape() { // "ESCAPE" KEY
  2054. if ( getAttr('#content_pane','data-loaded') !== 'loaded' ) { removeAttr('#content_pane','data-loaded'); removeAttr('#content_iframe','src'); } // close loading iframe
  2055. switch(true) {
  2056. case !isTopWindow(): if ( hasClass('body','has_quicklook') ) { quickLookThis('close'); } else { messageSend('top_body','focus_top'); } break; // focus top from iframe content
  2057. case document.activeElement.tagName.toLowerCase() !== 'body': case hasClass('body','focus_content') && !hasClass('body','has_menu'): focusEl('#top_body'); break; // focus top from non-iframe content
  2058. case document.activeElement.tagName.toLowerCase() === 'body': // if top already focussed...
  2059. switch(true) {
  2060. case hasClass('body','has_menu'): case hasClass('body','has_menu_stats'): menuClose(); break; // close menu or
  2061. default: /* closeContent('esc'); */ focusEl('#top_body'); // close content
  2062. }
  2063. }
  2064. document.onmousemove = null; window.getSelection().removeAllRanges(); window.stop(); // remove text selections; stop loading; cancel mousemove event watcher
  2065. removeClass('body','has_overlay'); removeClass('.dirlist_item','dirlist_subdir_loading');
  2066. getEls('.dirlist_item.selected,.dirlist_item.content_loaded').forEach( el => el.classList.remove('selected','content_loaded') );
  2067. getEls('.dirlist_item.media').forEach( (el) => { if ( el.querySelector('.dirlist_item_media_duration').innerHTML === '') { setMediaDuration( el.id,el.dataset.kind,Number.NaN )} } ); // set loading durations to error
  2068. getEls('.show_input input')?.forEach( el => el.value = ''); removeClass('.show_input','show_input');
  2069. if ( hasClass('body','has_warning') || hasClass('body','has_help') ) { getEls('#warning_btn_cancel,#close_help').forEach( el => el.click() ); } // close warnings or help
  2070. }
  2071. function eKey_Period(e) { // close loading iframe
  2072. window.stop(); removeClass('.dirlist_item','dirlist_subdir_loading'); if ( hasClass('body','has_warning') ) { e.preventDefault(); getEl('#warning_btn_cancel,#close_help').click(); }
  2073. if ( getAttr('#content_pane','data-loaded') !== 'loaded' ) { closeContent('iframe'); }
  2074. }
  2075. function eKey_Space(e) { // "SPACE" KEY
  2076. switch(true) {
  2077. case isTopWindow():
  2078. switch(true) {
  2079. case hasContent(['audio','ignore']): case hasContent('video'): e.preventDefault(); mediaPlayPause(); break; // media play/pause
  2080. case ( hasContent('image') || hasContent('font_specimen')) && hasContent('hidden_grid'): e.preventDefault(); closeContent(); break; // close grid image
  2081. case hasContent('font_file_glyph'): case hasContent('font_specimen_glyph'): e.preventDefault(); closeFont(); break; // close glyph
  2082. case hasContent('font') && getEl('.font_glyph_item.selected') !== null: e.preventDefault(); showFontGlyph(null,getEl('.font_glyph_item.selected').id); break; // show glyph
  2083. case hasContent('grid') && getEl('.grid_item.selected') !== null: e.preventDefault(); getEl('.grid_item.selected').click(); break; // show grid items
  2084. } break;
  2085. case !isTopWindow(): // not top window
  2086. switch(true) {
  2087. case hasClass('body','is_html'): case hasClass('body','is_text'): return;
  2088. case elExists('.dirlist_item.audio_loaded') && !hasClass('body','has_quicklook'): e.preventDefault(); messageSend('top_body','iframe_play_pause_media'); break; // play/pause top media if no quicklook
  2089. case hasClass('body','has_quicklook'): e.preventDefault(); quickLookThis('close'); break; // close quicklook
  2090. case getEl('.dirlist_item.selected') !== null: e.preventDefault(); quickLookThis(getEl('.dirlist_item.selected').id,getData('.dirlist_item.selected','kind')); break; // show quicklook
  2091. }
  2092. }
  2093. }
  2094. function eKey_Tab(e) { e.preventDefault(); let incr = ( e.shiftKey === true ? 1 : -1 ); navigateByTabKey(e,incr); } // "TAB" KEY
  2095. function eKey_A(e) { selectMultipleItems(e); } // "A" KEY Select all dir items with cmd_key
  2096. function eKey_E(e) { // "E" KEY
  2097. switch(true) {
  2098. case hasClass('body','has_warning'): break;
  2099. case cmdShiftKey(e): eStopPrevent(e);
  2100. if ( !isTopWindow() ) { messageSend('top_body','toggle_texteditor'); } else { getEl('#show_texteditor a').click(); }
  2101. addClass('#top_body','focus_content'); break; // toggle text editor
  2102. case cmdKey(e): eStopPrevent(e); if ( !isTopWindow() ) { messageSend('top_body','toggle_menu'); } else { menuShow(e,'sidebar_menu_main_container'); } break; // toggle main menu
  2103. }
  2104. }
  2105. function eKey_R(e) { // "R" KEY
  2106. switch(true) {
  2107. case cmdKey(e) && !isTopWindow(): e.preventDefault(); messageSend('top_body','reload'); break; // send reload message to top
  2108. case cmdKey(e) && hasClass('#content_body','texteditor_edited'): e.preventDefault(); showWarning('resetContent'); break; // warn before reloading edited iframe text files from textarea
  2109. case cmdKey(e): if ( !hasContent() ) { return true; } else { e.preventDefault(); getEl('#reload_btn').click(); } break; // reload window if no content open else reload/reset content
  2110. }
  2111. }
  2112. function eKey_W(e) { // "W" KEY
  2113. switch(true) {
  2114. case !isTopWindow(): e.preventDefault(); messageSend('top_body','close'); break; // send close message to top
  2115. case hasClass('body','has_help'): getEl('#close_help').click(); break; // close help
  2116. case hasContent('audio') && hasContent('null'): closeContent('audio'); break; // close audio when nothing else open
  2117. case ( /has_\w+list/.test(getClassNames('body')) && !hasContent() ): showWarning('closeContent',['closePlaylist','false']); break; // close playlist
  2118. case !hasContent('null'): e.preventDefault(); closeContent(); break; // close content
  2119. default: return; // else close window (or normal behavior)
  2120. }
  2121. }
  2122. getEls('#top_body, #content_body').forEach( el => el.onkeydown = function(e) { if ( window.parent !== window.top ) { return; } // ===> MAIN KEYDOWN EVENTS (prevent keydown in quicklooked window)
  2123. let active_el = document.activeElement, active_el_tag = active_el.tagName.toLowerCase(); // allow default: buttons, inputs, selects, textareas
  2124. switch(true) {
  2125. case e.key === 'Enter':
  2126. switch(true) {
  2127. case (/ui_font/.test(active_el.id)): uiPrefFontSet(e); break;
  2128. case (/go_to_item_input/.test(active_el.id)): goToItem(e); break;
  2129. default: eKey_Enter(e,active_el); break; // Key = Enter/Return
  2130. }
  2131. case ( /button|input|select|textarea/.test(active_el_tag) && !/escape|tab|shiftkey|metakey|altkey/.test(e.key.toLowerCase()) && !( cmdKey(e) && /r|w|-|=/.test(e.key) )): return; // prevent/allow certain key combos
  2132. case active_el.hasAttribute('contentEditable') && !(/escape|tab|shiftkey|metakey|altkey/.test(e.key.toLowerCase()) && !( cmdKey(e) && /r|w|-|=/.test(e.key) )): return; // ...in certain situations.
  2133. case e.key === 'Escape': eKey_Escape(); break;
  2134. case (/has_warning|has_help/.test(getClassNames('body')) && !( cmdKey(e) || (/escape|tab|shiftkey|enter/.test(e.key.toLowerCase()) ) ) ): // nobreak
  2135. case e.key === 'shiftKey' && ( hasClass('body','has_warning') || hasClass('body','has_help') ): if (e.key !== 'Enter' && e.key !== 'Tab') { e.preventDefault(); return false; } break; // Key = Shift
  2136. case ( /Arrow/.test(e.key) ): arrowKeyFunctions(e,false,el); break; // (e,bool,selected_el.id); id for dblclick iframe item
  2137. case e.key === ' ': eKey_Space(e); break; // Key = Space
  2138. case ( e.key && !e.metaKey && !e.altKey && !e.ctrlKey && e.key !== 'Tab' && !/Arrow/.test(e.key) ): navigateByTypedStr(e); break; // alphanumeric navigation
  2139. case e.key === 'a' && cmdKey(e): eKey_A(e); break; // Cmd + a: select all
  2140. case e.key === 'd' && cmdShiftKey(e) && !hasClass('body','has_warning'): e.preventDefault(); getEl('#show_details').click(); break; // Cmd/Ctrl + D: Toggle Details
  2141. case e.key === 'e': eKey_E(e); break; // Cmd/Ctrl + E: Toggle Main Menu or Text Editor
  2142. case e.key === 'g' && cmdKey(e) && ( hasClass('#top_body','has_images') || hasClass('#top_body','has_fonts') ): e.preventDefault(); getEl('#show_grid_btn').click(); break; // Show grids
  2143. case e.key === 'i' && cmdShiftKey(e): if ( !isTopWindow() ) { messageSend('top_body','toggle_invisibles'); } else { getEl('#show_invisibles_container input').click(); } break; // Toggle invisibles
  2144. case e.key === 'j' && cmdShiftKey(e): goToItem(e); break;
  2145. case e.key === 'o' && cmdShiftKey(e): window.open( getAttr('.dirlist_item.selected a','href') ); break; // Cmd+Shift+O: Open in new window
  2146. case e.key === 'r': eKey_R(e); break; // Cmd/Ctrl + Shift + R: Refresh
  2147. case e.key === 'w' && cmdKey(e): eKey_W(e); break; // KEY = W && Cmd/Ctrl: close content
  2148. case e.key === '=' && cmdKey(e) && hasContent('grid,image,font,glyph'): e.preventDefault(); contentHeaderButtons(e,'increase'); break; // Cmd/Ctrl + equals: scale larger
  2149. case e.key === '-' && cmdKey(e) && hasContent('grid,image,font,glyph'): e.preventDefault(); contentHeaderButtons(e,'decrease'); break; // Cmd/Ctrl + hyphen: scale smaller
  2150. case e.key === '\\': eKey_BackSlash(e); break; // KEY = \ BACKSLASH
  2151. case e.key === 'Tab': eKey_Tab(e); break; // KEY = TAB
  2152. case e.key === '.' && cmdKey(e): eKey_Period(e); break; // click cancel button
  2153. }
  2154. });
  2155. // ***** END EVENT LISTENER INITIALIZATION
  2156. //============================//
  2157. function menuShow(e,id) { // ===> SHOW MENUS
  2158. if ( e !== null ) { e.stopPropagation(); }
  2159. if ( /sidebar_menu/.test(id) && /has_menu/.test(getClassNames('body')) ) { menuClose(); return; } // close menu on click if open
  2160. removeClass('body','has_top_menu has_menu has_menu_parents has_menu_stats is_blurred,show_sidebar_false'); removeClass('#sidebar_menu_main *','selected hovered show_input'); // remove classes
  2161. let el, menu_el = getEl('#'+id), menu_el_classlist = menu_el.classList, parent_nav_id, time, position = getEl('#sidebar_header_utilities_row_1').offsetTop - 1 + 'px'; // get clicked menu element
  2162. switch(true) {
  2163. case id === 'sidebar_menu_main_container': setStyle('#'+ id +' > ul','top',position); addClass('#top_body','has_menu'); break; // show main menu
  2164. case id === 'sidebar_menu_parents': setStyle('#'+ id +' > ul','top',position); addClass('body','has_menu_parents'); break; // show sidebar_menu_parents
  2165. case id === 'stats_summary': setStyle('#stats_details_items','height',getEl('#stats_container').height - getEl('#stats_details_summary').height - 4); // show stats menu
  2166. addClass('body','has_menu_stats'); break; // update durations, set classes
  2167. case menu_el_classlist.contains('has_submenu'): removeClass('li.has_submenu','selected hovered'); menu_el.classList.toggle('selected'); break; // toggle open submenu
  2168. case ( /about_link|donate_link|contact_link/.test(id) ): changeLocation([menu_el.href,'external']); break; // external menu links
  2169. case id === 'cuesheet_track_list_container_audio': case id === 'cuesheet_track_list_container_video': // onmouseenter cue_sheet...
  2170. el = getEl('#'+id); setStyle('#'+id+' > div','top',el.offsetTop + el.clientHeight - 5 +'px'); break; // set y position of cuesheet track list
  2171. case id.startsWith('cuesheet_item_'): // click cuesheet track list items
  2172. switch(true) {
  2173. case menu_el.classList.contains('selected'): mediaPlayPause(); break; // play/pause if already selected
  2174. default: // otherwise select new cuesheet track list item
  2175. parent_nav_id = menu_el.closest('nav').id; time = getEl('#'+id).getAttribute('data-duration'); // N.B.: cue time format is mm:ss:ff (ff = frames, 75 frames/sec)
  2176. addRemoveClassSiblings('#'+ id,'selected');
  2177. switch(parent_nav_id) {
  2178. case 'cuesheet_track_list_container_audio': if ( time < getEl('#audio').duration ) { getEl('#audio').currentTime = time; } break;
  2179. case 'cuesheet_track_list_container_video': if ( time < getEl('#content_video').duration ) { getEl('#content_video').currentTime = time; } break;
  2180. }
  2181. } break;
  2182. }
  2183. }
  2184. function menuClick() { getEl('#sidebar_menu_main .selected:not(.hovered)')?.querySelectorAll('a,span,label')[0]?.click(); if ( hasClass('body','focus_content') ) { messageSend('iframe','menu_close'); } } // ===> CLICK MENU
  2185. function menuClose() { removeClass('body','has_top_menu has_menu has_menu_parents has_menu_stats is_blurred'); removeClass('#sidebar_menu_main *','selected hovered show_input'); } // ==> CLOSE MENUS
  2186. function statsGetHoveredListClass(el) { let this_class = '.'+ (el?.dataset?.kind?.split(', ')); // ===> GET HOVERED STATS CLASS
  2187. switch(true) {
  2188. case el.id === 'stats_details_summary_dirs': this_class = '.dir'; break;
  2189. case el.id === 'stats_details_summary_files': this_class = '.file'; break;
  2190. case this_class === '.dir': this_class = '.dir:not(.ignored):not(.invisible):not(.app)'; break;
  2191. case this_class === '.dir.app': this_class = '.dir.app:not(.ignored):not(.invisible)'; break;
  2192. } return this_class;
  2193. }
  2194. //============================//
  2195. // DIRLIST CLICK AND SELECT FUNCTIONS
  2196. function clickDirListItem(e,id) { e.preventDefault(); let src = getEl('#'+ id +' a').href; // ===> CLICK DIR LIST ITEM
  2197. switch(true) {
  2198. case window.parent !== window.top: return;
  2199. case ( !/rowid/.test(id) ): break; // null if not a dirlist item
  2200. case e.metaKey && /'app|dir|font|image/.test(getClassNames('#'+id)): selectMultipleItems(e,id); break; // select multiple items on click
  2201. case !isTopWindow(): e.stopPropagation(); showThis(id,true,true); // iframe dirlist items
  2202. if ( hasClass('#'+id,'audio_loaded') ) { messageSend('top_body','iframe_play_pause_media'); } break; // play/pause iframe audio onclick
  2203. case hasClass('#'+id,'audio'): if ( hasClass('#'+id,'audio_loaded') ) { mediaPlayPause(); removeClass('.dirlist_item.selected','selected'); addClass('#'+id,'selected'); return } // else...showThis:
  2204. showThis(id,false,true); break;
  2205. case hasClass('#'+id,'video'): if ( /youtube.com|youtu.be/.test(src) ) { showThis(id); return; }
  2206. if ( hasClass('#'+id,'content_loaded') ) { mediaPlayPause(); } else { showWarning( 'showThis',[id] ); } break; // '' ''
  2207. default:
  2208. switch(true) {
  2209. case hasClass('body','texteditor_edited') || ( /has_\w+list/.test(getClassNames('#top_body') ) ): showThis(id); break; // if top edited, show item (i.e.hide text editor)
  2210. case hasClass('body','iframe_edited'): messageSend('iframe','unloading','',['showThis',id]); break; // if iframe edited, show warning
  2211. default: showWarning( 'showThis',[id] ); // default: show content with warning
  2212. }
  2213. }
  2214. }
  2215. function iframeClickLink(e,id,link) { let url, kind; // ===> IFRAME CLICK LINKS from html files
  2216. if ( !link.startsWith('#') ) { url = newURL(link); if ( e !== null ) { e.preventDefault(); } } // if link is not a link fragment, create url, prevent default
  2217. switch(true) {
  2218. case link.startsWith('#'): case url.href.startsWith('file:///?'): case url === undefined: break; // allow default link fragment behavior
  2219. case id === 'tbody': window.location = link + '?&show_directory_source=true'; break; //
  2220. case id === 'iframe_parent_link': messageSend('top_body','show_iframe_parent','',[getEl('#iframe_parent_link').href,'dir','iframe_parent']); break; // send message "show_iframe_parent"
  2221. case url.protocol === 'file:' && window.location.protocol !== 'file:': messageSend('top_body','local_link'); break; // show warning when attempting to open local links from non-local pages
  2222. case url.protocol !== 'file:' && window.location.protocol === 'file:': window.open(link,'_blank'); break; // open remote link from local page in new tab/window
  2223. case url.protocol === 'file:' && window.location.protocol === 'file:': // nobreak; open local links to local files in iframe
  2224. case url.protocol === 'about:': // nobreak; document #link fragments
  2225. case RegExp(url.hostname).test(window.location.hostname): // nobreak; same origin links (might not include TLD) (just covering bases)
  2226. case RegExp(window.location.hostname).test(url.hostname): // no break; same origin links (might not include TLD) (just covering bases)
  2227. kind = getLinkInfo(url.href)[3];
  2228. if ( /dir|app/.test(kind) ) { messageSend('top_body','show_iframe_dir','',[link,kind,id] ); } else { messageSend('top_body','show_iframe_file','',[link,kind,id] ); } break;
  2229. default: window.open(link,'_blank'); break; // else open external document links in new tab
  2230. }
  2231. }
  2232. function iframedblClickThis(e,id,link) { e.stopPropagation(); // ===> IFRAME DOUBLECLICK THIS iframe dir_list items (files and dirs)
  2233. if ( /_/.test(id) ) { id = id.split('_')[0]; } // temp: if double-clicking a subdir item, id === top parent item id; we'd like to send full subdir item id so that it can be reopened when the subdir item is closed.
  2234. let el = getEl('#'+id);
  2235. let kind = el.dataset.kind, message = ( /dir|app/.test(kind) ? 'show_iframe_dir' : 'show_iframe_file' ); // get item kind
  2236. if ( kind === 'audio' ) { removeClass('.dirlist_item.audio','audio_loaded selected'); el.classList.add('audio_loaded','selected'); } // iframe audio
  2237. messageSend('top_body',message,'',[link,kind,id]); // send message
  2238. }
  2239. function iframeClick(e,id,kind,link) { e.preventDefault();
  2240. switch(true) {
  2241. case window.parent !== window.top: e.stopPropagation(); return;
  2242. case kind === 'dblclick': case e.type === 'dblclick': iframedblClickThis(e,id,link); break;
  2243. case kind === 'dirlist_item': clickDirListItem(e,id); break;
  2244. case kind === 'link': iframeClickLink(e,id,link); break;
  2245. }
  2246. }
  2247. //============================//
  2248. // ===> CLICK TOGGLE UI PREF ELEMENTS
  2249. function uiPrefToggleOnClick(e,id) { e.stopPropagation(); e.stopImmediatePropagation(); showWarning('uiPrefToggle',getEl('#'+id).getAttribute('data-ui_pref') ); }
  2250. function uiPrefToggle(pref_id) { // ===> TOGGLE UI PREFS: and update searchParams
  2251. let settings_value = [pref_id,UI_Settings[pref_id]].join('_'), current_value = getCurrentUIPref(pref_id), new_value = getNewUIPref(pref_id);
  2252. let message_target = ( isTopWindow() ? 'iframe' : 'top_body' ), send = 'false';
  2253. if ( /button|label|select|input/.test( document.activeElement.tagName.toLowerCase() ) ) { document.activeElement.blur(); } // blur any focused form elements
  2254. switch(true) {
  2255. case ( pref_id === 'show_image_thumbnails'): uiPrefImgThumbsToggle(new_value[1]); send = 'true'; break;
  2256. case ( /texteditor_|text_editing/.test(pref_id) ): textEditorTogglePrefs(pref_id); send = 'true'; break; // Text Editor Preferences
  2257. case !hasClass('#content_body','show_details_false') && pref_id === 'show_details' && !isTopWindow(): // nobreak; hide iframe details on first toggle
  2258. case new_value[1] === 'false': addClass('body',pref_id +'_false'); searchParamSet(pref_id,'false'); send = 'true'; break;
  2259. case new_value[1] === 'true': case current_value === 'false': removeClass('body',pref_id +'_false'); searchParamDelete(pref_id); send = 'true'; break;
  2260. case ( /sort_by_/.test(pref_id) ): uiPrefSortToggle(pref_id); break; // toggle sorting
  2261. case ( /theme|theme_light|theme_dark/.test(pref_id) ): uiPrefThemeToggle(new_value); break; // toggle light/dark theme
  2262. case new_value.length > 1 && settings_value === new_value.join('_'): // nobreak; new value === settings value
  2263. default: // other non-booleans
  2264. searchParamSet( getNewUIPref(pref_id)[0],new_value.join('_') ); // set searchParam
  2265. removeClass('body',[pref_id,current_value].join('_')); addClass('body',new_value.join('_')); // remove old bodyclass; add new bodyclass
  2266. send = 'true';
  2267. }
  2268. if ( send === 'true' && isTopWindow() && !/show_details|show_sidebar/.test(pref_id) ) { messageSend(message_target,'uiPrefToggle','',pref_id); } // send message to iframe
  2269. if ( /show_invisibles|show_ignored_items/.test(pref_id) ) { updateStats(); } // update stats if necessary
  2270. }
  2271. function uiPrefsReset(e) { eStopPrevent(e); // ===> DEFAULT SETTINGS: remove queries;
  2272. if ( window.confirm( 'Are you sure you want to remove all your temporary UI settings from the URL query string?' ) ) {
  2273. removeClass('body','has_menu');
  2274. let query_str = '',selected_str = '',history_str = '';
  2275. if ( getCurrentUIPref('selected').length && getCurrentUIPref('selected').match(/[0-9\+]+?/) ) { selected_str += 'selected='+ getCurrentUIPref('selected'); }
  2276. if ( getCurrentUIPref('history').length && getCurrentUIPref('history').match(/[0-9\+]+?/) ) { history_str += 'history='+ getCurrentUIPref('history'); }
  2277. query_str = history_str +'&'+ selected_str;
  2278. if ( query_str.length > 1 ) { query_str = '?'+ query_str.replace(/\s/g,'+'); }
  2279. window.location.assign(window_location + query_str);
  2280. }
  2281. }
  2282. //============================//
  2283. function uiPrefThemeToggle(new_value) { // ===> TOGGLE UI PREF THEME
  2284. new_value = new_value.join('_');
  2285. removeClass('body','theme_dark theme_light'); addClass('body',new_value);
  2286. if ( new_value === 'theme_light' ) { searchParamDelete( 'theme' ); } else { searchParamSet( 'theme','dark' ); }
  2287. if ( isTopWindow() ) { messageSend('iframe','uiPrefToggle','',new_value); } // send message iframe
  2288. }
  2289. function uiPrefFontSet(e) { eStopPrevent(e); let value; // ===> SET UI FONT
  2290. switch(true) {
  2291. case e?.type === 'click': getEl('#ui_font input').value = getCurrentUIPref('ui_font'); addClass('#ui_font','show_input'); getEl('#ui_font input').focus(); break; // show input & current ui font on click
  2292. case e?.key === 'Enter': value = getEl('#ui_font input').value; // get the entered ui font
  2293. if ( value !== '' ) { document.body.style.fontFamily = value; searchParamSet('ui_font',value); } else { document.body.style.fontFamily = null; searchParamDelete('ui_font'); }
  2294. messageSend('iframe','set_ui_font','',value);
  2295. removeClass('#ui_font','show_input'); menuClose(); break; // set the font; close menu
  2296. default: if ( UI_Prefs_Non_Bool.ui_font !== getCurrentUIPref('ui_font') ) { document.body.style.fontFamily = getCurrentUIPref('ui_font'); } // set the ui font on page load
  2297. }
  2298. }
  2299. function uiPrefScaleSet(e,value,bool) { // bool === true --> from buildUI // ===> SET UI SCALE
  2300. if ( e !== null ) { e.stopPropagation(); }
  2301. switch(true) {
  2302. case Math.round(value) === 100: document.body.style.removeProperty('transform'); document.body.style.removeProperty('width'); document.body.style.removeProperty('height'); // remove body styles
  2303. setData('#ui_scale .menu_item','value','100%'); getEl('#ui_scale_input').value = 100; // reset input
  2304. searchParamDelete('ui_scale'); break; // delete the searchParam
  2305. default:
  2306. document.body.style.transform = 'scale('+value+'%)'; document.body.style.width = Math.round(10000/Number(value))+'%'; document.body.style.height = Math.round(10000/Number(value))+'%'; // add body styles
  2307. searchParamSet('ui_scale',Math.round(value)); // set the searchParam
  2308. }
  2309. if ( Number(value) < 100 ) { document.documentElement.style.width = Math.round(10000/Number(value))+'%'; } else { document.documentElement.style.removeProperty('width'); } // scale the html element if value < 1
  2310. if ( bool === true ) { setData('#ui_scale .menu_item','value',value+'%'); getEl('#ui_scale_input').value = value; } // set the input on load
  2311. }
  2312. function uiPrefImgThumbsToggle(bool) { // ===> TOGGLE UI PREF IMG THUMBS
  2313. let image_files = getEls('.dirlist_item.image'), current_background_image, max_count = 2000; // Add/remove image thumbnails as background icons
  2314. switch(true) {
  2315. case getData('.stats_list_item.image a','count') > max_count && getCurrentUIPref('show_image_thumbnails_always') === 'false': // nobreak; don't show thumbs if show_image_thumbnails_always === false
  2316. case bool === 'false': addClass('body','show_image_thumbnails_false'); searchParamSet('show_image_thumbnails','false'); break;
  2317. default: removeClass('body','show_image_thumbnails_false'); searchParamDelete('show_image_thumbnails');
  2318. }
  2319. image_files.forEach( (image ) => {
  2320. current_background_image = image.querySelector('a .has_icon_before_before').style.backgroundImage; // get the current background_image, save for future toggle
  2321. switch(true) { // toggle thumbnail display
  2322. case getData('.stats_list_item.image a','count') > max_count && getCurrentUIPref('show_image_thumbnails_always') === 'false': // nobreak; don't show thumbs if show_image_thumbnails_always === false
  2323. case bool === 'false': // show default icon, don't remove existing thumbnail
  2324. image.querySelector('a .has_icon_before_before').style.backgroundImage = get_SVG_UI_File_Icon('file_icon_image') +','+ current_background_image; // only first background image is visible
  2325. break;
  2326. default: // remove default image icon or load image thumbnail
  2327. image.querySelector('a .has_icon_before_before').style.backgroundImage = 'url("'+ image.querySelector('a').href +'")';
  2328. image.querySelector('a .has_icon_before_before').dataset.image_url = 'url("'+ image.querySelector('a').href +'")';
  2329. }
  2330. });
  2331. }
  2332. function loadImageThumbnail(id) { let image = getEl('#'+id);
  2333. image.querySelector('a .has_icon_before_before').style.backgroundImage = 'url("'+ image.querySelector('a').href +'")';
  2334. image.querySelector('a .has_icon_before_before').dataset.image_url = 'url("'+ image.querySelector('a').href +'")';
  2335. }
  2336. function uiPrefSortToggle(pref_id) { // ===> TOGGLE UI SORT PREF
  2337. let current_sort_by = getCurrentUIPref('sort_by'), new_sort_by = pref_id.split('_').reverse()[0];
  2338. let current_sort_direction = getCurrentUIPref('sort_direction'), new_sort_direction = ( new_sort_by !== current_sort_by ? 'ascending' : getNewUIPref('sort_direction_'+ current_sort_direction)[1] );
  2339. switch(true) { // toggle sort_by
  2340. case new_sort_by !== current_sort_by: new_sort_by === 'default' ? searchParamDelete('sort_by') : searchParamSet('sort_by',new_sort_by); break;
  2341. case new_sort_by === current_sort_by: break;
  2342. }
  2343. switch(true) { // toggle sort_direction
  2344. case new_sort_by !== current_sort_by: // nobreak
  2345. case current_sort_direction === 'descending': searchParamDelete('sort_direction'); break; // delete search_param if new sort is ascending (default)
  2346. case current_sort_direction === 'ascending': searchParamSet('sort_direction','descending'); break; // add descending search_param
  2347. }
  2348. removeClass('body','has_menu sort_by_name sort_by_default sort_by_duration sort_by_size sort_by_date sort_by_kind sort_by_ext sort_direction_ascending sort_direction_descending'); // remove all sorting body classes
  2349. addClass('body',pref_id,'sort_direction_'+ new_sort_direction);
  2350. subDirClose(); // close subdirs
  2351. // RE-SORT DIRECTORY ITEMS:
  2352. let has_dir = false, sorted, iframe_src, iframe_url, iframe_params, items_html_arr = [];
  2353. if ( isTopWindow() && hasContent('dir') ) { has_dir = true; iframe_src = getEl('#content_iframe').src; }
  2354. Array.from(getEls('.dirlist_item')).forEach(el => items_html_arr.push(el.outerHTML.replace(/border_bottom |border_top /g,''))); // get elements for new sort
  2355. sorted = sortDirListItems( Array.from(items_html_arr), pref_id, new_sort_direction ); // sort the items
  2356. getEl('#directory_list').innerHTML = sorted; // insert sorted items into dir_list
  2357. initDirListEvents(); initIframeEvents(); // re-initialize dir_list event listeners
  2358. switch(true) {
  2359. case hasContent('font_grid'): showGrid('show_font_grid'); break; // sort grids --> change this to actual sort, not reload
  2360. case hasContent('image_grid'): showGrid('show_image_grid'); break; // sort grids --> change this to actual sort, not reload
  2361. case hasContent('grid'): showGrid('show_grid'); break; // sort grids --> change this to actual sort, not reload
  2362. case has_dir === true: // re-sort iframe directory
  2363. if ( elExists('.dirlist_item.selected') ) { showThis(getEl('.dirlist_item.selected').id); } // show the selected directory
  2364. iframe_url = new URL(iframe_src); // create url obj
  2365. iframe_params = new URLSearchParams(iframe_url.search) // create url search params
  2366. iframe_params.set('sort_by',pref_id.slice(pref_id.lastIndexOf('_') + 1)); // set sort_by
  2367. iframe_params.set('sort_direction',new_sort_direction); // set sort_direction params
  2368. iframe_url.search = iframe_params.toString(); // update url search params
  2369. getEl('#content_iframe').src = iframe_url.href; break; // reload the iframe with new src url
  2370. case elExists('.dirlist_item.selected:not(.audio)'): showThis(getEl('.dirlist_item.selected').id); break; // after sort, show selected item; don't autoloadcoverart
  2371. }
  2372. }
  2373. function textEditorTogglePrefs(pref_id,bool) { let args = []; // ===> TOGGLE TEXT EDITOR PREFERENCES (from menus or toolbar buttons); bool from UIsetup
  2374. switch(true) {
  2375. case ( /text_editing_enable/.test(pref_id) ): // toggle text editing
  2376. bool = ( hasClass('body','text_editing_enable_false') ? 'true' : 'false' )
  2377. switch(true) {
  2378. case bool === 'false':
  2379. addClass('body','text_editing_enable_false'); searchParamSet('text_editing_enable','false'); setAttr('#content_body #texteditor_raw_pane','readonly',''); // set readonly
  2380. removeClass('body','texteditor_split_view_true'); addClass('body','texteditor_split_view_false'); break;
  2381. default: removeClass('body','text_editing_enable_false texteditor_split_view_false texteditor_split_view_true texteditor_sync_scroll_false texteditor_sync_scroll_true');
  2382. searchParamDelete('text_editing_enable'); removeAttr('#content_body #texteditor_raw_pane','readonly'); // remove readonly
  2383. textEditorTogglePrefs('texteditor_split_view', ( getCurrentUIPref('texteditor_split_view') !== undefined ? getCurrentUIPref('texteditor_split_view') : 'true' ) );
  2384. textEditorTogglePrefs('texteditor_sync_scroll',( getCurrentUIPref('texteditor_sync_scroll') !== undefined ? getCurrentUIPref('texteditor_sync_scroll') : 'true' ) );
  2385. break;
  2386. } if ( isTopWindow() ) { messageSend('iframe','uiPrefToggle','','text_editing_enable'); } break;
  2387. case ( /texteditor_view_raw|texteditor_view_styled|texteditor_view_html/.test(pref_id) ): // toggle texteditor_preview & html panes
  2388. args = ['texteditor_view',pref_id.split('_').reverse()[0]]; bool = ( hasClass('body',pref_id) ? 'true' : 'false' ); // set args
  2389. removeClass('body','texteditor_view_raw texteditor_view_styled texteditor_view_html'); addClass('body',pref_id);
  2390. if ( bool === 'true' && hasClass('body',pref_id) ) { textEditorTogglePrefs('texteditor_split_view'); } // toggle split if same view clicked
  2391. break; // add pref_id body_class
  2392. case ( /texteditor_split_view/.test(pref_id) ):
  2393. bool = ( /true|false/.test(bool) ? bool : ( hasClass('body','texteditor_split_view_true') || hasClass('body','text_editing_enable_false') ) ? 'false' : 'true' ); // set bool
  2394. args = ['texteditor_split_view',bool]; addClass('body','texteditor_split_view_'+ bool); searchParamSet('texteditor_split_view'); // set args, add body_class, set search param
  2395. switch(true) {
  2396. case bool === 'true': removeClass('body','texteditor_split_view_false'); if ( getCurrentUIPref('texteditor_view') === 'raw') { addClass('body','texteditor_view_styled'); } break;
  2397. case bool === 'false': removeClass('body','texteditor_split_view_true'); focusEl('#texteditor_raw_pane'); break;
  2398. } break;
  2399. case ( /texteditor_sync_scroll/.test(pref_id) ): // sync_scroll
  2400. bool = ( /true|false/.test(bool) ? bool : getCurrentUIPref('texteditor_sync_scroll') === 'true' ? 'false' : 'true' ); args = ['texteditor_sync_scroll',bool]; // set bool and args
  2401. searchParamSet('texteditor_sync_scroll',bool); // set search param
  2402. if ( bool === 'false' ) { getEl('#texteditor_sync_scroll input').checked = false; } else { getEl('#texteditor_sync_scroll input').checked = true; } break;
  2403. }
  2404. if ( !isTopWindow() ) { messageSend('top_body','searchParamSet','',args); messageSend('top_body','menu_close'); } // send messages to top: set search param, close menu
  2405. }
  2406. //============================//
  2407. function resizeSidebar(e) { e.preventDefault(); // ===> RESIZE SIDEBAR/Content Pane
  2408. menuClose();
  2409. let sidebar = getEl('#sidebar'), startX = e.pageX, window_width = window.innerWidth, sidebar_width = sidebar.offsetWidth;
  2410. addClass('body','has_overlay'); // prevent interference from the rest of ui
  2411. document.onmousemove = (f) => { f.stopPropagation(); f.preventDefault(); let deltaX = f.pageX - startX;
  2412. if ( f.pageX > 230 && f.pageX < window_width - 200 ) { sidebar.style.width = ( sidebar_width + deltaX ) + 'px'; }
  2413. scrollThis('#directory_list','.selected',false); // true = instant scroll
  2414. };
  2415. document.onmouseup = (e) => { e.stopPropagation(); removeClass('body','has_overlay'); document.onmousemove = null; searchParamSet('width',sidebar.offsetWidth); };
  2416. }
  2417. //============================//
  2418. // ***** SORTING ***** //
  2419. function sortAddBorders(sorted) { let item_kinds; // ===> ADD SORTING BORDERS
  2420. for ( let i = 0; i < sorted.length - 1; i++ ) {
  2421. item_kinds = sorted[i].match(/data-kind=\"\w+?\" /)[0];
  2422. if ( sorted[i + 1].indexOf(item_kinds) === -1 ) { sorted[i] = sorted[i].replace(/class=\"/,'class="border_bottom '); } // add border class
  2423. }
  2424. return sorted;
  2425. }
  2426. function sortItems(items_html_arr,sort_type,sort_direction) { let sort_id = sort_type.split('_').reverse()[0]; // ===> SORT INDEX ITEMS
  2427. const new_sort = new Intl.Collator( undefined, { numeric: true, sensitivity: 'base' } );
  2428. let sorted = [], aName, bName, aData, bData; // aLevel, bLevel, aKind, bKind;
  2429. sorted = items_html_arr.sort( (a, b) => { // sorted items
  2430. // aLevel = a.dataset.level; bLevel = b.dataset.level; // subdirectory level
  2431. if ( !/data-name/.test(a) || !/data-name/.test(b) ) { null; } else { aName = a.replace(/(.+?)data-name="([^"]+?)"(.+)/g,'$2'); bName = b.replace(/(.+?)data-name="([^"]+?)"(.+)/g,'$2'); }// get data-name
  2432. switch(true) { // aData, bData = size, date, kind, ext, time
  2433. case !( new RegExp('data-'+ sort_id) ).test(a) || !( new RegExp('data-'+ sort_id) ).test(b): break;
  2434. default: aData = a.replace( ( new RegExp( '.+?data-'+ sort_id +'="([^"]+?)".+') ),'$1' ); bData = b.replace( ( new RegExp( '.+?data-'+ sort_id +'="([^"]+?)".+') ),'$1' );
  2435. }
  2436. switch(true) { // sort 'em!
  2437. case sort_direction === 'ascending': return ( new_sort.compare(aData, bData) === 0 ? new_sort.compare(aName, bName) : new_sort.compare(aData, bData) ); // A-Z
  2438. case sort_direction === 'descending': return ( new_sort.compare(bData, aData) === 0 ? new_sort.compare(bName, aName) : new_sort.compare(bData, aData) ); // Z-A
  2439. }
  2440. });
  2441. return sorted; // return sorted items
  2442. }
  2443. function sortDirListItems(items_html_arr, sort_type, sort_direction) { // ===> SORT DIR LIST on click
  2444. let sorted = [], sort_all = items_html_arr, sort_dirs = items_html_arr.filter( item => ( /data-kind=\"dir\"/.test(item) ) ), sort_files = items_html_arr.filter( item => ( !/data-kind=\"dir\"/.test(item) ) );
  2445. switch(true) {
  2446. case ( /sort_by_size|sort_by_date/.test(sort_type) ) && ( /has_\w+list/.test(getClassNames('#top_body')) ): return items_html_arr.join('\n'); // don't sort playlists by size/date
  2447. case sort_type === 'sort_by_default': // if sort default
  2448. const sorted_dirs = sortItems( sort_dirs, sort_type, sort_direction ), sorted_files = sortItems( sort_files, sort_type, sort_direction ); // ...sort dirs and files separately
  2449. switch(true) {
  2450. case sort_direction === 'ascending': // if sort ascending...
  2451. if ( sorted_files[0] !== undefined && sorted_dirs[0] !== undefined && !/sort_by_kind|sort_by_ext/.test(sort_type) ) {
  2452. sorted_files[0] = sorted_files[0].replace(/class=\"/,'class="border_top '); // add border class
  2453. }
  2454. sorted = [...sorted_dirs,...sorted_files]; // sorted = sorted_dirs.concat(sorted_files); // ...dirs before files
  2455. break;
  2456. case sort_direction === 'descending': // sort descending...
  2457. if ( sorted_dirs[0] !== undefined && !/sort_by_kind|sort_by_ext/.test(sort_type) ) { sorted_dirs[0] = sorted_dirs[0].replace(/class=\"/,'class="border_top '); } // add border class
  2458. sorted = [...sorted_dirs,...sorted_files]; // ...else files before dirs
  2459. }
  2460. break;
  2461. default: sorted = sortItems( sort_all, sort_type, sort_direction ); // other sorts (name, size, date): files and dirs together
  2462. }
  2463. if ( /sort_by_kind|sort_by_ext/.test(sort_type) ) { sorted = sortAddBorders(sorted); } // add borders for sort by kind and ext
  2464. return sorted.join('\n');
  2465. }
  2466. // ***** END BASIC UI FUNCTIONS ***** //
  2467. //============================//
  2468. // ***** CONTENT PANE ***** //
  2469. //============================//
  2470. function contentHeaderButtons(e,id) { eStopPrevent(e); let incr; // ===> CONTENT HEADER BUTTONS
  2471. switch(true) {
  2472. case ( /prev|next/.test(id) ): // PREV/NEXT item or audio track
  2473. let key = ( /prev_btn|prev_track/.test(id) ? 'ArrowLeft' : /next_btn|next_track/.test(id) ? 'ArrowRight' : null ); // define arrowkey
  2474. if ( hasClass('body','focus_content') ) { focusEl('#content_pane'); } else { getEl('#'+id).parentElement.blur(); }
  2475. navigateGetEl([key,false]); break; // get the next item
  2476. case ( /increase|decrease/.test(id) ): incr = ( hasContent('font_file') ? 1.0625 : 1.125 ); scaleItems(e,incr,id,getContentPaneData()); break; // SCALE BUTTONS; set scaling increment
  2477. }
  2478. }
  2479. // ===> SHOW INDIVIDUAL CONTENT TYPES
  2480. //============================// MEDIA
  2481. function showMedia(kind,id,src,bool) { let title = '';
  2482. switch(kind) {
  2483. case 'audio':
  2484. showMedia('close_video'); removeClass('#content_pane','has_audio_error');
  2485. switch(true) {
  2486. case id === 'content_iframe_file': // clicked iframe audio files
  2487. src = decodeURIComponentSafe(src); title = src.slice(src.lastIndexOf('/') + 1);
  2488. addClass('#content_pane','has_audio has_iframe_audio'); setAttr('#content_iframe_utility','src',src.slice(0,-4)); break;
  2489. default: // dir_list audio files
  2490. switch(true) {
  2491. case hasClass('#'+ id,'local'): break;
  2492. case bool === 'true': // bool !== undefined: if from autoLoadItems, just select file (don't add .audio_loaded class)
  2493. if ( getEl('.dir.content_loaded') !== undefined ) { addRemoveClassSiblings('.dir.content_loaded','selected'); } // select dir.selected instead of media
  2494. addClass('#'+ id,'selected'); break;
  2495. default: addRemoveClassSiblings('#'+ id,'audio_loaded selected'); // otherwise select loaded media
  2496. }
  2497. title = getEl('#'+id).querySelector('a').innerText; addClass('#content_pane','has_audio'); removeClass('#content_pane','has_iframe_audio'); src = getEl('#'+id).querySelector('a').getAttribute('href');
  2498. cuesheetGet(id,src,'audio'); // get cuesheet
  2499. }
  2500. autoLoadCoverArt(bool,id); setAttr('#audio','src', src ); setAttr('#audio','data-src_id', id );
  2501. getEl('#content_audio_title span').innerText = title; removeClass('#content_audio_playlist_item','has_content'); break;
  2502. case 'close_audio': // CLOSE AUDIO; pause media; needed in each case, not outside switch
  2503. getEl('#content_audio_title span').innerHTML = ''; removeAttr('#audio','data-src_id' );
  2504. removeClass('body','is_playing is_paused'); removeClass('.dirlist_item.audio_loaded','audio_loaded'); removeClass('#content_pane','has_audio has_iframe_audio has_audio_error');
  2505. removeClass('#content_audio_playlist_item','has_content'); messageSend('iframe','close_iframe_audio'); mediaPlayPause('close'); break;
  2506. case 'video': setAttr('#content_video','data-src_id', id ); // SHOW VIDEO
  2507. showMedia('close_audio'); cuesheetGet(id,src,'video'); setData('#content_pane','content','has_video'); addClass('#content_video','has_content'); break;
  2508. case 'close_video': // CLOSE VIDEO; pause media; needed in each case, not outside switch
  2509. removeClass('.dirlist_item.video.content_loaded','content_loaded'); removeClass('#content_video','has_content');
  2510. removeAttr('#content_video','src'); removeAttr('#content_pane','data-content'); mediaPlayPause('close'); break;
  2511. }
  2512. if ( id !== undefined && isNaN(Number(getThisDuration(id))) ) { setThisDuration(id,0); getMediaDuration( src,kind,id ); } // reset media duration if necessary.
  2513. }
  2514. //============================// MEDIA PLAYBACK
  2515. function mediaGetUpdatedShuffleArray() { let playlist = []; // ===> UPDATE TRACKLIST (for shuffle play)
  2516. Array.from(getEls('.dirlist_item.media:not(.unchecked,.disabled)')).forEach( el => playlist.push( el.id ) ); return playlist; // don't include currently selected item
  2517. }
  2518. function mediaShuffleArray(array) { // ===> SHUFFLE ARRAY: Randomize Shuffle List
  2519. for ( let i = array.length - 1; i > 0; i-- ) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array;
  2520. }
  2521. function mediaShuffleListUpdate(id,bool) { let shuffle_list; // ===> UPDATE SHUFFLE LIST; bool === false: there is a selected media item
  2522. switch(true) {
  2523. case !hasClass('body','shuffle_media'): return; // don't shuffle if normal playback
  2524. case id !== undefined: shuffle_list = getData('#content_audio','shufflelist').split(','); // handle checked and unchecked media items: id = checked/unchecked item.id
  2525. switch(true) {
  2526. case bool === false: case hasClass('#'+ id,'unchecked'): shuffle_list = shuffle_list.filter( shuffle_item_id => shuffle_item_id !== id ); break; // remove selected or unchecked items
  2527. default: shuffle_list.push(id); shuffle_list = mediaShuffleArray( shuffle_list ); // else add re-checked items to shufflelist
  2528. } break;
  2529. default: shuffle_list = mediaShuffleArray( mediaGetUpdatedShuffleArray() ); showThis(shuffle_list[0]); shuffle_list.shift(); // reset shufflelist when shuffle option checked; load first item in list
  2530. }
  2531. setData('#content_audio','shufflelist',shuffle_list); setData('#shuffle_label','shufflecount',' ('+shuffle_list.length+' remaining)'); // set shufflelist data and remaining count
  2532. }
  2533. function mediaShuffleGetNextItem() { // ===> GET NEXT SHUFFLED ITEM
  2534. let shuffle_list = getData('#content_audio','shufflelist').split(','), shuffled_item_id = shuffle_list.pop(); setData('#content_audio','shufflelist',shuffle_list); return shuffled_item_id;
  2535. }
  2536. function toggleCheckBox(e,id) { let input_el = getEl('#'+id).querySelector('input'); input_el.checked = !input_el.checked; input_el.blur(); }
  2537. function toggleChecked(e,id) { e?.stopPropagation();
  2538. let el = getEl('#'+id), input_el = getEl('#'+id).querySelector('input'); input_el.checked = !input_el.checked; el.classList.toggle('unchecked'); mediaShuffleListUpdate(id);
  2539. }
  2540. function toggleAllChecked(e) { e.stopPropagation(); getEls('.dirlist_item.media').forEach( el => toggleCheckBox(e,el.id) ); mediaShuffleListUpdate(); } // ===> TOGGLE ALL MEDIA CHECKBOXES; update shufflelist
  2541. function mediaSkip(e,args) { let factor, skip; // ===> MEDIA SKIP +/-10/30 seconds
  2542. switch(true) {
  2543. case e !== undefined: factor = ( e.key === 'ArrowLeft' ? -1 : 1 ); skip = ( e.altKey && e.shiftKey ? 30 : e.altKey ? 10 : null ); break; // from top
  2544. case args !== undefined: factor = ( args[0] === 'ArrowLeft' ? -1 : 1 ); skip = args[1] || 0; break; // from iframe
  2545. }
  2546. const player = ( hasContent('video') ? getEl('#content_video') : getEl('#audio') );
  2547. const time = player.currentTime; // get current time
  2548. player.currentTime = time + factor*(skip); // set new time
  2549. }
  2550. function mediaIsPlaying(id) { // ===> IS PLAYING; returns true if all conditions are true
  2551. switch(true) {
  2552. case !isTopWindow() && !hasClass('body','has_quicklook'): return ( hasClass('body','is_playing') ? true : false );
  2553. default: return ( id !== undefined && getEl('#'+id).currentTime > 0 && !getEl('#'+id).paused && !getEl('#'+id).ended );
  2554. }
  2555. }
  2556. function mediaPlayPause(task) { let player_el = ( hasContent('video') ? getEl('#content_video') : getEl('#audio') ), playing = mediaIsPlaying( player_el.id ); // ===> PLAY/PAUSE MEDIA
  2557. switch(true) {
  2558. case player_el === null: break;
  2559. case task === 'close': player_el.removeAttribute('src'); // nobreak;
  2560. case task === 'pause': case playing === true: try { player_el.pause(); } catch(e) { null; } break;
  2561. case task === 'play': case playing === false: try { player_el.play(); } catch(e) { null; } break;
  2562. }
  2563. if ( hasContent('iframe_audio') ) { messageSend('iframe','setIframePlayerStatus','',task); }
  2564. }
  2565. function mediaPlayPrevNextIframeItem() { if ( !isTopWindow() ) { getEl('.dirlist_item.selected.media a').trigger('dblclick'); messageSend('top_body','iframe_play_pause_media'); } } // ===> PLAY PREV/NEXT IFRAME MEDIA ITEM
  2566. function mediaPlaybackOptions(e,id) { e.stopPropagation(); // ===> AUDIO PLAYBACK OPTIONS (shuffle, loop)
  2567. let loop_el = getEl('#loop'), shuffle_el = getEl('#shuffle');
  2568. switch(true) {
  2569. case id === 'loop': case id === 'loop_media_files': document.body.classList.toggle('loop_media'); break;
  2570. case id === 'shuffle': case id === 'shuffle_media_files': document.body.classList.toggle('shuffle_media'); mediaShuffleListUpdate(); break;
  2571. }
  2572. switch(true) { // change audio checkboxes prop
  2573. case id === 'loop_media_files': ( loop_el.checked === true ? loop_el.checked = false : loop_el.checked = true ); break;
  2574. case id === 'shuffle_media_files': ( shuffle_el.checked === true ? shuffle_el.checked = false : shuffle_el.checked = true ); break;
  2575. }
  2576. if ( shuffle_el.checked === false ) { setData('#shuffle_label','shufflecount',''); } // remove shufflecount
  2577. }
  2578. //============================// PLAYLISTS/FILELISTS
  2579. function openPlaylist(file_name,reader,data) { let bool, list_class; // files & reader = open .m3u file; data = m3u.txtfile content// // ===> OPEN PLAYLIST
  2580. if ( !data.startsWith('#EXTM3U') ) { return; } // prevent reading non-playlist files
  2581. if ( !/has_\w+list/.test(getClassNames('body') ) ) { // if body does not already have playlist or filelist...
  2582. let body_classes = document.body.classList; // ...store original dir_list and body "has_"classes as data
  2583. setData('#directory_list','dir_list',getEl('#directory_list').innerHTML); setData('#directory_list','body_classes',body_classes.value); // store the original dir_list and body classes
  2584. body_classes.forEach( (body_class) => { if ( body_class.startsWith('has_') ) { removeClass('body', body_class ); } }); // remove media and other body classes
  2585. }
  2586. file_name = ( file_name !== '' ? file_name : getEl('.dirlist_item.selected.playlist .dirlist_item_name_a').innerText ); // get the file name for the title and current_dir_path
  2587. getEl('#current_dir_path span').innerHTML = file_name; // set sidebar header title
  2588. let new_index = buildNewIndex( '', prepPlaylist(data),'','playlist' ); // build the new dir_list
  2589. addClass('body',new_index[1]); // add playlist body classes (has_media, has_audio, etc.)
  2590. list_class = ( new_index[1].split(' ').every((el) => ['has_audio','has_media','has_video'].includes(el)) ? 'has_playlist' : 'has_filelist' ); addClass('body',list_class); // set play- or filelist class
  2591. if ( /file:/.test(new_index) && !/file:/.test(window_protocol) ) { addClass('body','has_warning'); addClass('#directory_list','local'); showWarning('openPlaylist','warning_local_playlist'); } // warn local on non-local
  2592. getEl('#directory_list').innerHTML = new_index[0]; // replace dir_list with prepared playlist
  2593. autoLoadItems(); // if autoload media...
  2594. scrollThis('#directory_list','.selected',false); document.title = 'Playlist: '+ file_name; bool = true;
  2595. initEvents(); updateStats(bool);
  2596. }
  2597. function closePlaylist() { // files & reader = open .m3u file; data = m3u.txtfile content// // ===> OPEN PLAYLIST
  2598. window.stop(); closeContent('audio'); closeContent(); removeClass('body'); // close all content, remove all body classes, stop window resource loading
  2599. addClass('body',getData('#directory_list','body_classes')); // restore original body classes
  2600. getEl('head title').innerText = 'Index of '+ window_location; // restore window title
  2601. getEl('#current_dir_path span').innerHTML = current_dir_path; // restore current_dir_path tilte
  2602. getEl('#directory_list').innerHTML = getData('#directory_list','dir_list'); // restore original dir_list...
  2603. deleteData('#directory_list','dir_list'); deleteData('#directory_list','body_classes'); // ...and remove data
  2604. showThis(getEl('.dirlist_item.audio.audio_loaded')?.id); showThis(getEl('.dirlist_item.content_loaded')?.id); // reload previously loaded content
  2605. initEvents(); updateStats(false);
  2606. }
  2607. function playlistMake() { let items, playlist = []; // ===> MAKE PLAYLIST file from directory files for export as m3u
  2608. let playlist_type = getEl('#make_playlist_form input:checked').id;
  2609. switch(playlist_type) { // get playlist items according to selected type
  2610. case 'media_files_only': items = getEls('.dirlist_item.media:not(.unchecked)'); break;
  2611. case 'audio_files_only': items = getEls('.dirlist_item.audio:not(.unchecked)'); break;
  2612. case 'video_files_only': items = getEls('.dirlist_item.video:not(.unchecked)'); break;
  2613. case 'all_non_media_files': items = getEls('.dirlist_item.non_media'); break;
  2614. case 'all_items': items = getEls('.dirlist_item'); break;
  2615. case 'directories_only': items = getEls('.dirlist_item.dir'); break;
  2616. case 'files_only': items = getEls('.dirlist_item.file'); break;
  2617. }
  2618. switch(true) {
  2619. default:
  2620. items.forEach( (item) => { playlist.push( playlistMakeItem(item.id,true) ); }); // make playlist entry for each item
  2621. playlist = '#EXTM3U\n'+ playlist.join('\n'); // add playlist header id
  2622. saveFile( playlist,'audio/mpeg-url',(getEl('#current_dir_path').innerText).split('/').reverse()[1] +'.m3u' ); // save m3u with default name = current dir name
  2623. closeWarning(); // close warning
  2624. }
  2625. }
  2626. // Make playlist entry for display in title bar
  2627. function playlistMakeItem(id,bool) { //*** id = 'content_title' or 'content_audio_title', bool = true --> from playlistMake(), otherwise from playlistShowItem(); ***//
  2628. let title, link, duration, full_path = window.location.protocol + window.location.hostname + window.location.pathname;
  2629. title = ( hasContent('grid') && id === 'content_title' ? 'Files from: '+ full_path : bool === true ? getEl('#'+id).dataset.name : getEl('#'+id).innerText ); // get title txt
  2630. switch(true) { // Get link
  2631. case bool === true: link = getEl('#'+id).querySelector('a').href; break; // get item link for filelists
  2632. case hasContent('grid') && id === 'content_title': link = full_path; break; // grid link = dir path
  2633. case id === 'content_title': link = ( hasContent('image') ? getAttr('#content_image','src') : getAttr('.content_el.has_content','src') ); break; // content link
  2634. case id === 'content_audio_title': link = getLinkInfo( getAttr('#audio','src') )[0].trim(); break; // audio link
  2635. }
  2636. link = ( link.startsWith('/') ? window.location.protocol +'//'+ link : link ); // fix links without protocols (local files)
  2637. link = new URL(link); // make new URL from link
  2638. link = link.protocol +'//'+ link.hostname + link.pathname; // compose link
  2639. duration = ( hasContent('video') ? Number.parseInt(getEl('#video').duration) : id === 'content_audio_title' ? Number.parseInt(getEl('#audio').duration) : '' );
  2640. return '#EXTINF:'+ duration +','+ title +'\n'+ link +'\n'; // return composed playlist entry
  2641. }
  2642. function playlistShowItem(id) { // ===> SHOW PLAYLIST ENTRY
  2643. switch(true) {
  2644. case id === 'close': removeClass('#content_playlist_item,#content_audio_playlist_item','has_content'); getEls('#content_header textarea').forEach(el => el.value = ''); return;
  2645. case hasContent('audio_error') && id === 'content_audio_title': closeContent('audio');
  2646. case !hasContent():
  2647. case hasContent('texteditor'): return;
  2648. }
  2649. let el_id = ( id === 'content_title' ? 'content_playlist_item' : id === 'content_audio_title' ? 'content_audio_playlist_item' : '' );
  2650. document.getElementById(el_id).classList.toggle('has_content');
  2651. document.getElementById(el_id).querySelector('textarea').value = playlistMakeItem(id);
  2652. textareaSelectContent(document.getElementById(el_id).querySelector('textarea').id); // add entry to the textarea
  2653. document.getElementById(el_id).querySelector('textarea').focus();
  2654. }
  2655. //============================// CUESHEETS
  2656. function cuesheetGet(id,link,kind) { // id = 'content_iframe_file' or 'dir_list.media.id', link = selected.href, kind = audio/video // ===> GET CUE SHEET
  2657. if ( id === 'content_iframe_file' ) { return; } // prevent error for iframe files
  2658. removeClass('.cuesheet_track_list_container','has_cue_sheet'); // reset cuesheet container
  2659. getEl('#cuesheet_track_list_audio').innerHTML = ''; // empty existing cue sheet track list
  2660. let media_file_name = decodeURIComponentSafe(getEl('#'+id).dataset.name);
  2661. let cue_file_name = decodeURIComponentSafe(media_file_name).slice(0,media_file_name.lastIndexOf('.')) + '.cuetxt', cue_file_link = '';
  2662. let cue_file = getEl('.dirlist_item.code[data-name="'+ cue_file_name +'"'); // get the cuesheet id
  2663. if ( cue_file !== null ) {
  2664. cue_file_link = document.getElementById( cue_file.id ).querySelector('a').href.trim();
  2665. getEl('#content_iframe_utility').src = cue_file_link; // set utility iframe src for processing
  2666. switch(kind) { // get durations
  2667. case 'audio': setAttr('#cuesheet_track_list_container_audio','data-duration',getEl('#audio').duration); break;
  2668. case 'video': setAttr('#cuesheet_track_list_container_video','data-duration',getEl('#content_video').duration); break;
  2669. }
  2670. }
  2671. }
  2672. function cuesheetProcess(cuesheet_text) { // ===> PROCESS CUE SHEET
  2673. let cuesheet_tracks, track, prepped_track, prepped_track_list = [], track_header, track_id, display_time, index, index_sec, container_id, commands_arr = ['PERFORMER','TITLE','INDEX'], track_command, classes = 'cuesheet_track display_grid background_grey_85 pointer padding_4_6';
  2674. cuesheet_text = cuesheet_text.replace(/\t/g,' ');
  2675. cuesheet_tracks = ( !cuesheet_text.startsWith('TRACK') ? cuesheet_text.slice(cuesheet_text.indexOf('TRACK ')).split('TRACK ').reverse() : cuesheet_text.split('TRACK ').reverse() );
  2676. for ( track of cuesheet_tracks ) { prepped_track = []; track = track.trim().split(/[\n\r]/); track_id = track.shift().split(' ')[0]; // for each track in the cue sheet...
  2677. for ( let i = 0; i < commands_arr.length; i++ ) { // and for each command in commands_arr...
  2678. for ( track_command of track ) { // and for each command in the track
  2679. if ( track_command.match(commands_arr[i]) ) {
  2680. track_command = track_command.trim().replace(/^(performer|title|index\s+\d+)\s*/mgi,'').replace(/^('|\"|\&quot;)|('|\"|\&quot;)$/mgi,''); // prep the displayed track information
  2681. // format INDEX command; N.B.: time format = mm:ss:ff (ff = frames @ 75fr/sec):
  2682. if ( commands_arr[i] === 'INDEX' ) { display_time = track_command.replace(/INDEX\s+\d+\s+/,''); index = display_time.split(':').reverse(); index_sec = index[0]/75 + index[1]*1 + index[2]*60; }
  2683. prepped_track[i] = '<span class="cue_'+ commands_arr[i].toLowerCase() +'">'+ ( track_command.length === 0 ? '—' : track_command ) +'</span>';
  2684. }
  2685. }
  2686. }
  2687. prepped_track.unshift(`<li id="cuesheet_item_${track_id}" class="${classes}" data-duration="${index_sec}"><span class="cue_index align_right">${track_id}</span>`); // add prepped_track prefix html
  2688. prepped_track_list.push(prepped_track.join('')); // add prepped_track to prepped_track_list
  2689. }
  2690. prepped_track_list.pop(); prepped_track_list = prepped_track_list.reverse().join('</li>') +'</li>'; // remove mystery empty track item; create prepped track list
  2691. track_header = '<li class="cuesheet_track header display_grid cursor_default background_grey_85 border_top border_bottom bold"><span class="cue_track_id"></span><span class="cue_performer">Performer</span><span class="cue_title">Title</span><span class="cue_index align_right" title="mm:ss:ff">Time</span></li>';
  2692. switch(true) {
  2693. case elExists('.dirlist_item.audio_loaded'): case hasContent('iframe_audio'): container_id = '#cuesheet_track_list_audio'; break;
  2694. case elExists('.dirlist_item.video.content_loaded'): case hasContent('iframe_file'): container_id = '#cuesheet_track_list_video'; break;
  2695. }
  2696. getEl(container_id)?.closest('nav')?.classList.add('has_cue_sheet'); getEl(container_id)?.insertAdjacentHTML('beforeend',track_header + prepped_track_list); // add cue sheet track list to menu
  2697. initCuesheetEvents(); // init cuesheet event listeners
  2698. }
  2699. //============================// FONTS
  2700. function fontRestoreOptions(id) { // restore font toolbar values after closing previewed glyph with modified options values
  2701. getEl('#font_size').value = getEl(id).dataset.scale; getEl('#text_color').value = getEl(id).style.color; // restore size and color
  2702. switch(true) { // restore text stroke color & width
  2703. case id === '#font_file_grid':
  2704. getEl('#text_stroke_width').value = getEl(id).querySelector('svg path').getAttribute('stroke-width')?.replace(/[A-z]+/,'') || '';
  2705. getEl('#text_stroke_color').value = getEl(id).querySelector('svg path').getAttribute('stroke') || ''; break;
  2706. case id === '#font_specimen_viewer':
  2707. getEl('#text_stroke_width').value = getEl(id).style.webkitTextStrokeWidth.replace(/[A-z]+/,''); getEl('#text_stroke_color').value = getEl(id).style.webkitTextStrokeColor; break;
  2708. }
  2709. }
  2710. function showFont(id,bool,font_grid,link,i,font_items_length) { // ===> SET FONT CSS rules or create font grid items (bool === true) // id from gridMakeFontItems(); link = from previewed directory
  2711. let font_styles = getEl('#font_styles'), border_class = '', last_item_class = '', display_name, font_grid_item_info = '', font_grid_item_el = '';
  2712. let font_family = ( link !== '' ? link?.slice(link.lastIndexOf('/') + 1,link?.lastIndexOf('.')) : getEl('#'+id).dataset.name ); font_family = decodeURIComponentSafe(font_family); // get CSS font family
  2713. let font_url = ( link !== '' ? link : getAttr('#'+ id +' a','href') ); font_url = decodeURIComponentSafe(font_url); // get CSS font src
  2714. switch(true) {
  2715. case bool === false: // If bool === false, set CSS rules for previewed fonts
  2716. font_styles.innerHTML = `@font-face { font-family: "${ font_family }"; src: url("${ font_url }"); }`; // insert new @font-face rule
  2717. addClass('#content_pane','has_font_specimen');
  2718. getEl('#font_specimen_viewer').style.fontFamily = '"'+font_family+'"'; // set content font styles
  2719. getEl('#font_specimen_viewer').style.fontSize = getEl('#font_size').value +'em'; // "" ""
  2720. getEl('#font_specimen_viewer').style.lineHeight = ( Number(getEl('#line_height').value) === 0 ? 1.2 : Number(getEl('#line_height').value) + 1.2 ); // "" ""
  2721. getEl('#font_specimen_viewer').style.letterSpacing = getEl('#letter_spacing').value; // "" ""
  2722. getEl('#font_specimen_viewer').style.webkitTextStrokeWidth = getEl('#text_stroke_width').value; // "" ""
  2723. getEl('#text_color').value = getEl('#font_specimen_viewer').style.color;
  2724. getEl('#text_stroke_color').value = getEl('#font_specimen_viewer').style.webkitTextStrokeColor; break;
  2725. case font_grid === 'font_grid': // ...else (if bool === true) make grid font items
  2726. display_name = font_family;
  2727. if ( i === font_items_length - 1 ) { last_item_class = 'border_bottom_x'; }
  2728. if ( i > 0 ) { border_class = 'border_top_x'; }
  2729. font_grid_item_info = `<p class="font_grid_item_info margin_0 text_color_default font_size_small line_height_1">${ display_name.toUpperCase() }</p><h2 style=\'font-family: "${ font_family }"\'; class="margin_0 normal"><a class="text_color_default" href="${ font_url }">${ display_name.slice(0,font_family.lastIndexOf(".")) }</a></h2>`;
  2730. font_grid_item_el = `<li class="grid_item font_grid_item ${border_class} ${last_item_class} background_grey_90" data-id="${ id }" data-kind="font">${ font_grid_item_info }</li>`;
  2731. if ( getEl('#font_grid_styles').innerHTML.indexOf(font_family) === -1 ) { // only add font family style if it isn't already there
  2732. getEl('#font_grid_styles').insertAdjacentHTML('beforeend', `@font-face { font-family: "${ font_family }"; src: url("${ font_url }"); }`);
  2733. }
  2734. return font_grid_item_el;
  2735. }
  2736. initFontPreviewEvents();
  2737. }
  2738. function closeFont() {
  2739. switch(true) {
  2740. case hasContent('iframe_file,iframe_dir'): showThis( getEl('.dirlist_item.non_media.selected')?.id ); break; // if iframe item, reopen sidebar dir
  2741. case hasContent('font_file_glyph'): // CLOSE FONT_FILE_GLYPH font_specimen_grid
  2742. removeClass('#content_pane','has_font_file_glyph'); addClass('#content_pane','has_font_file');
  2743. getEl('#svg_container g').style.transform = 'scale(1)'; fontRestoreOptions('#font_file_grid'); break; // reset font_scale slider
  2744. case hasContent('font_specimen_glyph'): // CLOSE FONT_SPECIMEN_GLYPH
  2745. removeClass('#content_pane','has_font_specimen_glyph'); addClass('#content_pane','has_font_specimen');
  2746. removeAttr('#font_specimen_glyph','style'); removeAttr('#font_specimen_viewer',['data-char','data-unicode_hex']);
  2747. getEl('#font_specimen_glyph').innerText = ''; fontRestoreOptions('#font_specimen_viewer'); break; // reset font_scale slider
  2748. case hasContent('font_file'): showWarning( 'showFont','close' ); break; // CLOSE FONT FILE with warning
  2749. case getEl('#font_specimen_grid').children?.length > 0: getEl('#font_specimen_grid').innerHTML = ''; getEl('#unicode_char_ranges_select').value = '' ; showContentPaneEl('font'); break;
  2750. case hasContent('font_specimen'): if ( getEl('.dirlist_item.dir.selected') ) { showThis( getEl('.dirlist_item.dir.selected').id ); } // reopen selected sidebar dir
  2751. else { setData('#content_pane','content','has_null'); setData('#content_pane','loaded_id','null'); // else close sidebar font
  2752. removeClass('#content_pane','has_font_specimen has_file'); removeClass('#content_font','has_content'); setContentTitle('close'); } break;
  2753. default: setAttr('#content_pane','data-content','has_null'); removeClass('#content_pane','has_font_specimen has_font_specimen_glyph has_font_file has_font_file_glyph');
  2754. removeClass('#content_font','has_content'); removeAttr('#content_font','src'); removeAttr('#font_specimen_viewer','style');
  2755. setContentTitle('close'); focusEl('#top_body'); break;
  2756. }
  2757. }
  2758. function showFontGlyph(e,id) { e?.stopPropagation(); // ===> SHOW INDIVIDUAL GLYPH
  2759. let this_glyph, glyph_viewer, data_glyph_SVG, glyph_name, unicode_dec, unicode_hex, svg_container;
  2760. if ( ( e?.type === 'click' || e?.key === 'Enter' ) && /glyph_container/.test(id) ) { removeClass('#font_file_grid .selected','selected'); addClass('#'+id,'selected'); } // select clicked or
  2761. if ( !hasContent('font_file_glyph') && !hasContent('font_specimen_glyph') ) { getEl('#font_size').value = 1; }
  2762. switch(true) {
  2763. case id === 'close': case id === 'close_specimen': closeContent('font'); return; // close font glyph
  2764. case ( /font_specimen/.test(id) ): case hasContent('font_specimen'): case hasContent('font_specimen_glyph'): // font specimens and font specimen glyphs
  2765. this_glyph = getEl('#'+ id); glyph_viewer = getEl('#font_specimen_viewer');
  2766. getEl('#font_specimen_glyph').innerText = String.fromCodePoint(this_glyph.dataset.unicode_dec); // add glyph to specimen glyph
  2767. glyph_name = String.fromCodePoint(this_glyph.dataset.unicode_dec); unicode_dec = this_glyph.dataset.unicode_dec; unicode_hex = this_glyph.dataset.unicode_hex;
  2768. removeClass('#content_pane','has_font_specimen'); addClass('#content_pane','has_font_specimen_glyph');
  2769. focusEl('#content_pane'); getEl('#content_pane .font_glyph_item.selected')?.scrollIntoView({behavior:"smooth",block:"nearest"}); break; // hide font_specimen, show font_specimen glyph
  2770. case hasContent('font_file'): case hasContent('font_file_glyph'): // font files and font file glyphs
  2771. this_glyph = getEl('#'+id) || getEl('#font_file_grid .selected'); glyph_viewer = getEl('#font_file_glyph_viewer');
  2772. data_glyph_SVG = this_glyph.querySelector('svg').cloneNode(true); // get glyph by id, glyph path, & glyph SVG
  2773. data_glyph_SVG.setAttribute('viewBox','0 0 50 160'); data_glyph_SVG.style.width = '100%'; data_glyph_SVG.style.height = '100%'; data_glyph_SVG.style.color = getEl('#text_color').value; // set attrs & styles
  2774. data_glyph_SVG.classList.remove('invert'); data_glyph_SVG.classList.add('overflow_visible'); data_glyph_SVG.querySelector('g').style.transform = 'scale('+ glyph_viewer.dataset.scale +')';
  2775. if ( elExists('#svg_container') ) { // if a glyph is being shown...
  2776. getEl('#svg_container svg').remove(); getEl('#svg_container').insertAdjacentHTML('beforeend',data_glyph_SVG.outerHTML); svg_container = getEl('#svg_container').outerHTML; // get svg_container, add new svg;
  2777. } else { // ...and so preserve any added styles
  2778. svg_container = '<div id="svg_container" class="display_flex invert position_relative width_100 height_100">'+ data_glyph_SVG.outerHTML +'</div>'; // else create new svg container
  2779. }
  2780. getEl('#glyph_container')?.remove(); // remove existing glyph_container
  2781. glyph_viewer.insertAdjacentHTML('beforeend','<div id="glyph_container" class="flex_justify_center">'+ svg_container +'</div>'); // add the glyph_container to font_file_glyph_viewer
  2782. glyph_name = this_glyph.dataset.glyph_name; unicode_dec = this_glyph.dataset.unicode_dec; unicode_hex = this_glyph.dataset.unicode_hex;
  2783. getEl('#svg_container').onmousedown = function(e) { fontGlyphMove(e,'#svg_container') }; // init fontGlyphMove
  2784. removeClass('#content_pane','has_font_file'); addClass('#content_pane','has_font_file_glyph');
  2785. focusEl('#content_pane'); getEl('#content_pane .font_glyph_item.selected')?.scrollIntoView({behavior:"smooth",block:"nearest"}); break; // hide font_file grid, show font_file_glyph
  2786. }
  2787. glyph_viewer.dataset.glyph_name = glyph_name; glyph_viewer.dataset.unicode_dec = 'Unicode Dec: '+ unicode_dec; glyph_viewer.dataset.unicode_hex = 'Unicode Hex: '+ unicode_hex;
  2788. }
  2789. function openFontFile(files,reader,bool) { // ===> OPEN FONT FILE
  2790. getEl('#font_file_viewer').remove(); getEl('#content_font').insertAdjacentHTML( 'beforeend', Content_Pane_Elements('content_font_viewer') ); // reset font_file_viewer
  2791. switch(true) {
  2792. case files === 'close': // close font file
  2793. removeClass('body','focus_content');
  2794. removeAttr('#content_pane','data-content'); removeClass('#content_pane','has_font_specimen has_font_specimen_glyph has_font_file has_font_file_glyph'); // remove content_pane attrs and classes
  2795. removeClass('#content_font','has_content'); // remove content_font classes
  2796. getEl('#font_file_grid').innerHTML = ''; getEl('#content_title span').innerHTML = ''; // empty font_file_grid and content_title
  2797. if ( bool === undefined ) { // show previously loaded content or close content, but not if bool
  2798. if ( getEl('.dirlist_item.content_loaded') ) { showThis(getEl('.dirlist_item.content_loaded')?.id); } else { showThis('close'); } // (but not when opening new font file)
  2799. }
  2800. break;
  2801. default: // show font file
  2802. if ( !hasContent('font_file') && !/has_\w+list/.test(getClassNames('#top_body')) ) { showThis('close'); } // close content & hide grids
  2803. removeClass('#content_pane','has_font_specimen has_font_specimen_glyph has_font_file_glyph'); addClass('#content_pane','has_font_file'); // content_pane classes
  2804. setContentPaneAttrs(files.name,'font','content_font',files.name);
  2805. showContentPaneEl('font');
  2806. setData('#font_file_glyph_viewer','scale','1'); // reset font file glyph viewer
  2807. makeFontGlyphItems('font_file',reader.result); // => make glyph items from font file
  2808. getEls('.glyph_container').forEach( el => el.addEventListener('click', (e) => showFontGlyph(e,el.id))); // init click listener for each glyph
  2809. focusEl('#font_file_grid'); // focus font_file_grid
  2810. }
  2811. }
  2812. function getFontFileInfo(font) { let font_names = font.names, font_info_details = '', name, value; // ===> GET FONT INFO
  2813. for ( name in font_names ) {
  2814. value = font_names[name].en;
  2815. if ( name.endsWith('URL') ) {
  2816. let href = ( !value.startsWith('http') ? 'http://'+ value : value); // in case url without protocol is used
  2817. value = '<a class="bold" href="'+ href +'" target="_blank">'+ value +'</a>';
  2818. }
  2819. font_info_details += `<li class="display_none"><span class="col_1 font_info_name align_right"> ${name}: </span><span class="font_info_value">${value}</span></li>`;
  2820. }
  2821. let font_info = `<ul id="font_info" class="info_list font_size_small border_all margin_0 padding_0 text_color_default background_grey_80 position_fixed z_index_2"> <li class="info_list_header align_center bold"><span>FONT INFO:${font.names.fullName.en.toUpperCase()}</span></li> ${font_info_details} <li class="display_none"><span class="col_1 font_info_name align_right">numGlyphs: </span><span class="font_info_value">${font.numGlyphs}</span></li> </ul>`;
  2822. getEl('#font_file_viewer').insertAdjacentHTML('beforeend',font_info);
  2823. }
  2824. // FONT GLYPHS
  2825. function makeFontGlyphItem(kind,index,glyph) { // MAKE FONT GLYPH GRID ITEM; "index" is for font_specimens, "glyph" for font_files
  2826. let glyph_svg,glyph_index,glyph_name,glyph_dec,glyph_hex,glyph_path,glyph_boundingbox, tab_order = ( index === 0 ? 'data-tab_order="0"' : ''); // glyph_width,glyph_height,glyph_advancewidth,glyph_leftsidebearing;
  2827. switch(kind) {
  2828. case 'font_file':
  2829. glyph_name = glyph.name; glyph_index = glyph.index;
  2830. glyph_dec = ( glyph.unicode !== undefined ? glyph.unicode : '' ); glyph_hex = ( glyph_dec !== null ? convertDecimal2Hex(glyph_dec,4) : ''); // get glyph dec and hex values
  2831. glyph_path = glyph.getPath(0,100,72); glyph_boundingbox = glyph_path.getBoundingBox();
  2832. glyph_path = glyph_path.toSVG().replace(/"/g,'\'').replace(/path /g,'path fill=\'currentColor\' '); // define glyph svg path and escape "
  2833. glyph_svg = `<svg xmlns=\'http://www.w3.org/2000/svg\' x=\'0px\' y=\'0px\' viewBox=\'${(glyph_boundingbox.x1 < 0 ? 0 : glyph_boundingbox.x1)} 20 ${Math.abs(glyph_boundingbox.x2)} ${glyph_boundingbox.y2}\' xml:space=\'preserve\' preserveAspectRatio=\'xMidYMid meet\'><g>${glyph_path}</g></svg>`; break;
  2834. case 'font_specimen': glyph_index = index; glyph_dec = index; glyph_hex = convertDecimal2Hex(index,4); break;
  2835. }
  2836. return `<li id="${kind}_glyph_container_${glyph_index}" data-id="glyph_container_${glyph_index}" data-glyph_name="${glyph_name || ''}" ${tab_order} data-unicode_dec="${glyph_dec}" data-unicode_hex="U+${glyph_hex}" class="grid_item font_glyph_item glyph_container flex_justify_center border_right_x border_bottom_x position_relative"><div class="glyph display_block">${glyph_svg || String.fromCharCode(glyph_index)}</div></li>`; // return glyph item
  2837. }
  2838. function makeFontGlyphItems(kind,data,bool) { // MAKE FONT GLYPH GRIDS; kind,data === font_specimen,char_block_id or font_file,fontblob
  2839. let glyph_items = '', font_file, font_glyphs, font_file_grid = getEl('#font_file_grid'), loop_start, loop_end;
  2840. switch(kind) {
  2841. case 'font_file': // display font_file glyphs
  2842. font_file = window.opentype.parse(data); // parse font (req opentype.js)
  2843. font_glyphs = font_file.glyphs; // get font glyphs
  2844. setData('#font_file_glyph_viewer','font_name',font_file.names.fullName.en); // add font name to glyph viewer dataset
  2845. loop_start = 0, loop_end = font_glyphs.length; break;
  2846. case 'font_specimen': // for displaying unicode char_block ranges
  2847. if ( data !== '' ) { loop_start = convertHex2Decimal(getEl('#'+data).dataset.block_start); loop_end = convertHex2Decimal(getEl('#'+data).dataset.block_end) + 1; } break; // loop char_block start/end range
  2848. }
  2849. for ( let i = loop_start; i < loop_end; i++ ) { glyph_items += makeFontGlyphItem(kind,i,font_glyphs?.glyphs?.[i]); } // loop make glyph items for glyph grids
  2850. switch(true) {
  2851. case bool: return glyph_items;
  2852. case data === '': getEl('#font_specimen_grid').innerHTML = ''; break; // close font_specimen_grid
  2853. case kind === 'font_file': getFontFileInfo(font_file); font_file_grid.insertAdjacentHTML('beforeend',glyph_items); initFontPreviewEvents(); break;
  2854. case kind === 'font_specimen': getEl('#font_specimen_grid').innerHTML = glyph_items.trim(); initFontPreviewEvents(); focusEl('#content_pane'); break;
  2855. }
  2856. }
  2857. function fontGlyphMove(e,sel) { e.stopPropagation(); let startX = e.pageX, startY = e.pageY, elOffsetLeft = getEl(sel).offsetLeft, elOffsetTop = getEl(sel).offsetTop; // Move glyphs
  2858. document.onmousemove = function(e) { mouseMove(e,sel,startX,startY,elOffsetLeft,elOffsetTop); }
  2859. }
  2860. function fontGlyphSave() { // ===> SAVE GLYPH SVG
  2861. let data = getEl('#svg_container svg').cloneNode(true), file_name = getData('#font_file_glyph_viewer','font_name') +'_'+getData('#font_file_glyph_viewer','glyph_name');
  2862. data.setAttribute('viewBox','0 0 120 120'); data.removeAttribute('class'); data.querySelector('g').removeAttribute('style');
  2863. saveFile(data.outerHTML,'image/svg+xml',file_name);
  2864. }
  2865. // FONT UTILITIES
  2866. function fontOptions(e,id,value,variant_prop,variant_value) { eStopPrevent(e); // value sets select el and font-feature-settings; if variant_prop exists, it is used to set font-variant value
  2867. let props = {'font_size':'font-size','line_height':'line-height','letter_spacing':'letter-spacing','text_color':'color','text_stroke_width':'-webkit-text-stroke-width','text_stroke_color':'-webkit-text-stroke-color'};
  2868. let prop = (variant_prop || props[id] || 'font-feature-settings'), units = ( /font_size|letter_spacing|text_stroke_width/.test(id) ? 'em' : '' ), text_stroke_color, current_stroke_color, fontElId;
  2869. let el = getEl('#'+id);
  2870. switch(true) { // font_toolbar row 1: #font_specimen_variants
  2871. case (/font_variant_select/.test(id)): setValue('#font_tag_textarea',value); el.focus(); break; // select variants/stylistic sets
  2872. case id === 'font_tag_textarea': // textarea font tags; set corresponding font_variant_select value
  2873. switch(true) {
  2874. case value === null: setValue('#font_tag_textarea',''); return; // if value === null: reset
  2875. case Number(value?.length) < 4: setValue('#font_variant_select',''); break; //
  2876. case value?.startsWith('cv'): setValue('#font_variant_select','cv01'); break; //
  2877. case value?.startsWith('nalt'): setValue('#font_variant_select','nalt'); break; //
  2878. case value?.startsWith('ornm'): setValue('#font_variant_select','ornm'); break; //
  2879. case value?.startsWith('salt'): setValue('#font_variant_select','salt'); break; //
  2880. case value?.startsWith('ss'): setValue('#font_variant_select','ss01'); break; //
  2881. case value?.startsWith('swsh'): setValue('#font_variant_select','swsh'); break; //
  2882. default: setValue('#font_variant_select',value.slice(0,4)); break; //
  2883. } break; // focus font_tag_textarea
  2884. case id === 'unicode_char_ranges_select': makeFontGlyphItems('font_specimen',value); getEl('#content_font').scroll(0,0); return; // unicode chars startsWith
  2885. case id === 'font_size': scaleFonts(null,value,'font_size'); return;
  2886. case id === 'line_height': value = Number(value) + 1.2; if ( value === 1.2 ) { el.value = 0; } break; // line-height; default 1.2
  2887. case id === 'letter_spacing': value = ( Number(value) < 0 ? value/50 : Math.pow(value/50,2) ); if ( value === 0 ) { el.value = 0; } break; // letter-spacing
  2888. case id === 'text_color': if ( value === null ) { el.value = ''; value = ''; } break; // if value === null: reset
  2889. case id === 'text_stroke_width': current_stroke_color = getEl('#text_stroke_color').value;
  2890. text_stroke_color = ( value < 0 ? 'white' : ( current_stroke_color === '' || /white|black/.test(current_stroke_color) ) && value > 0 ? 'black' : current_stroke_color )
  2891. getEl('#font_specimen_viewer')?.style.setProperty('-webkit-text-stroke-color',text_stroke_color);
  2892. value = Math.abs(Number(value)); if ( value === 0 ) { el.value = 0; getEl('#text_stroke_color').value = ''; } break; // if value === 0: reset
  2893. case id === 'text_stroke_color': if ( value === null ) { el.value = ''; fontOptions(null,'text_stroke_width',null); } // if value === null: reset
  2894. value = ( getEl('#text_stroke_width').value < 0 ? 'white' : getEl('#text_stroke_color').value !== '' ? getEl('#text_stroke_color').value : 'black' ); break;
  2895. }
  2896. switch(true) { // determine element(s) to target
  2897. case hasContent('font_file_glyph'): fontElId = ( /text_stroke/.test(id) ? '#svg_container path' : '#svg_container' ); break; // el = svg_container or svg path
  2898. case hasContent('font_file'): fontElId = ( /text_stroke/.test(id) ? '#font_file_viewer path' : '#font_file_grid' ); break; // el = svg_container or svg path
  2899. case hasContent('font_specimen_glyph'): fontElId = '#font_specimen_glyph'; break;
  2900. case hasContent('font_specimen'): fontElId = '#font_specimen_viewer'; break;
  2901. }
  2902. switch(true) { // apply style to font element
  2903. case hasContent('font_file') && /text_stroke/.test(id): // if font_file and text_stroke style...
  2904. current_stroke_color = getEl('#text_stroke_color').value;
  2905. text_stroke_color = ( getEl('#text_stroke_width').value < 0 ? 'white' : ( current_stroke_color === '' || /white|black/.test(current_stroke_color) ) && value > 0 ? 'black' : current_stroke_color )
  2906. prop = ( id === 'text_stroke_width' ? 'stroke-width' : 'stroke' ); getEls(fontElId).forEach( el => el.setAttribute(prop,value + units) );
  2907. if ( id === 'text_stroke_width' ) { getEls(fontElId).forEach( el => el.setAttribute('stroke',text_stroke_color) ); } break; // set prop on font file svg paths
  2908. default:
  2909. switch(true) {
  2910. case ( /^cv|^ss|salt|swsh|ornm|nalt/.test(value) || id === 'font_tag_textarea' ):
  2911. prop = 'font-feature-settings'; value = '"'+value.slice(0,4) +'" '+ value.slice(4); getEl('#font_tag_textarea').focus(); break;
  2912. case props[id] !== undefined: break;
  2913. case variant_value !== undefined: value = variant_value; break;
  2914. case value?.length < 4: value = ''; break;
  2915. }
  2916. getEl(fontElId)?.style.setProperty(prop,value + units); // default: set style properties
  2917. }
  2918. }
  2919. function fontReset(id) { let el_ids = ['font_tag_textarea','font_size','line_height','letter_spacing','text_color','text_stroke_width','text_stroke_color'], current_font;
  2920. switch(true) {
  2921. case id === 'reset':
  2922. setValue('#font_variant_select',''); el_ids.forEach( el_id => getEl('#'+el_id).value = null ); // default toolbar values
  2923. switch(true) {
  2924. case hasContent('font_specimen'): // reset font_specimen
  2925. current_font = getEl('#font_specimen_viewer').style.fontFamily; getEl('#font_specimen_viewer').removeAttribute('style'); getEl('#font_specimen_viewer').style.fontFamily = current_font; break;
  2926. case hasContent('font_file'): setData('#font_file_grid','scale','1'); // reset font_file
  2927. getEls('#font_file_grid, #font_file_grid svg g').forEach( el => el.removeAttribute('style')); getEls('#font_file_grid svg path').forEach( el => el.removeAttribute('stroke') ); break;
  2928. }
  2929. default: if ( id === 'font_size' && hasContent('font_file') ) { // reset all font options...
  2930. getEl('#font_file_grid').removeAttribute('style'); getEl('#font_file_grid').querySelectorAll('svg g').forEach(function(el) { el.style.transform = 'none' }); getEl('#'+id).value = 1;
  2931. } else {
  2932. fontOptions(null,id,null); // or reset individual font options by id
  2933. }
  2934. }
  2935. }
  2936. //============================// GRIDS
  2937. function showGrid(id) { let selected_ID = ( getEl('.dirlist_item.selected')?.id || undefined); // ===> SHOW GRID
  2938. if ( id !== undefined ) { gridMake(id); } // initial make grid items; else just unhide existing grid (see below)
  2939. addClass('#show_grid_btn','has_grid'); // add class to #show_grid_btn button
  2940. setAttr('#content_pane','data-content','has_grid'); // set content_pane data-content attribute
  2941. removeClass('#content_pane','has_hidden_grid has_dir has_file has_font_specimen has_zoom_image'); removeClass('#content_pane div','selected hovered'); // remove classes; show hidden grid
  2942. setContentTitle('','grid'); // set content title
  2943. addClass('#content_pane div[data-id="'+ selected_ID +'"]','selected'); // reselect selected grid item
  2944. focusEl('#content_grid'); getEl('#content_pane .selected')?.scrollIntoView({behavior:"smooth",block:"nearest"}); // focus grid, scroll into view
  2945. }
  2946. function closeGrid(id) { // ===> CLOSE GRID
  2947. switch(true) {
  2948. case id === 'hide': removeAttr('#content_pane','data-content'); addClass('#content_pane','has_hidden_grid'); break; // hide grid, e.g., when viewing a grid item or other item
  2949. default:
  2950. switch(true) {
  2951. case hasContent('font'): case hasContent('image'): showGrid(); break;
  2952. default:
  2953. removeAttr('#content_pane','data-content'); removeClass('#content_pane','has_image_grid has_font_grid has_zoom_image'); // remove #content_pane classes & data-content attribute
  2954. removeAttr('#content_grid','style'); getEl('#content_grid').innerHTML = ''; // remove #content_grid style & all grid els
  2955. removeClass('#show_grid_btn','has_grid'); // remove #show_grid_btn button class
  2956. focusEl('#top_body'); if ( getEl('.dirlist_item.hovered') !== null ) {
  2957. showThis(getEl('.dirlist_item.hovered').id); removeClass('.dirlist_item.hovered','hovered'); } else { showThis(getVisibleElsBySelector('.dirlist_item')[0].id); // focus sidebar; show 1st sidebar item
  2958. } break;
  2959. } break;
  2960. }
  2961. }
  2962. function showContentGridItem(e,id,src,kind) { closeGrid('hide'); // ===> SHOW GRID ITEM
  2963. switch(true) {
  2964. case elExists('#'+id): addRemoveClassSiblings('#'+ id,'selected','selected'); getEl('#'+id).click(); break; // normal grid item display
  2965. case !elExists('#'+id): showThis('',false,true,[src,kind]); break; // show grid items from closed subdirectory
  2966. } focusEl('#content_pane .has_content');
  2967. }
  2968. // ***** IMAGE/FONT GRID SETUP
  2969. function gridMakeFontItems(id) { // ===> FONT GRID ITEMS
  2970. let font_grid_items = '', font_files = ( id !== undefined ? getEls('#'+id) : getEls('.dirlist_item.font') ), new_grid_item, font_items_length = font_files.length;
  2971. for ( let i = 0; i < font_items_length; i++ ) { // for each font...
  2972. new_grid_item = showFont( font_files[i].id, true, 'font_grid','',i,font_items_length ); font_grid_items += new_grid_item; gridMakeCountItems(); // make new font_grid_item and add it to the font_grid_items
  2973. }
  2974. return font_grid_items; // return font_grid_items
  2975. }
  2976. function gridMakeImageItems(id) { // ===> IMAGE GRID ITEMS
  2977. let image_grid_items = '', this_id, this_link, exts, title_name;
  2978. let image_files = ( id !== undefined ? getEls('#'+id) : getEls('.dirlist_item.image:not(.ignored)') );
  2979. let image_files_length = image_files.length, classes = 'grid_item image_grid_item flex_justify_center border_right_x border_bottom_x';
  2980. for ( let i = 0; i < image_files_length; i++ ) {
  2981. this_id = image_files[i].id; this_link = image_files[i].querySelector('a').href; title_name = this_link.slice(this_link.lastIndexOf('/') + 1);
  2982. exts = Item_Kinds.image.filter( ext => !Item_Settings.ignored.includes(ext) ); // decide which image files can be displayed
  2983. if ( exts.includes(image_files[i].dataset.ext) ) { // if item ext is in the image extension array...
  2984. let item = `<li class="${ classes } background_grey_90" data-ID="${ this_id }" data-index="${ i }" data-kind="image"><a href="${ this_link }"><img src="${ this_link }" title="${ title_name }" loading="lazy" /></a></li>`; // make new image_grid_item
  2985. image_grid_items += item; gridMakeCountItems(); // ...add it to the image_grid_items
  2986. }
  2987. }
  2988. return image_grid_items; // return image_grid_items
  2989. }
  2990. function gridMakeCountItems() {
  2991. let count = (getAttr('#content_title span','data-grid_count_items') || '0').toString(); count = count.replace(/^.+(\d+).+/m,'$1'); count = Number(count) + 1; let str = ( count > 1 ? ' items' : ' item' );
  2992. setAttr('#content_title span','data-grid_count_items',' ['+ count.toString() + str +']' );
  2993. }
  2994. function gridMake(id) { let el, kind; // ===> MAKE GRIDS
  2995. if ( /rowid/.test(id) ) { el = getEl('#'+id); kind = el.dataset.kind; } else { getEl('#content_grid').innerHTML = ''; } // remove previous grid items or make single grid item
  2996. removeClass('#content_pane','has_hidden_grid has_image_grid has_font_grid'); removeAttr('#content_title span','data-grid_count_items'); // reset content_pane grid classes and data
  2997. switch(true) { // determine which grid type to make
  2998. case el !== undefined:
  2999. switch(kind) { // make single grid items on cmd-click
  3000. case 'font': getEl('#content_grid').insertAdjacentHTML('beforeend',gridMakeFontItems(id)); break; // make single font grid item
  3001. case 'image': getEl('#content_grid').insertAdjacentHTML('beforeend',gridMakeImageItems(id)); break; // make single image grid item
  3002. } break;
  3003. case id === 'show_font_grid': addClass('#content_pane','has_font_grid'); getEl('#content_grid').innerHTML = gridMakeFontItems(); break; // make font grid
  3004. case id === 'show_image_grid': addClass('#content_pane','has_image_grid'); getEl('#content_grid').innerHTML = gridMakeImageItems(); break; // make image grid
  3005. default: getEl('#content_grid').innerHTML = gridMakeImageItems() + gridMakeFontItems(); // make image and font grid
  3006. }
  3007. initGridItemEvents(); // register event watchers for added grid elements
  3008. }
  3009. // ***** IMAGE/FONT/GLYPH SCALE
  3010. function scaleItems(e,incr,id,kind) { addClass('#reload_btn','reset'); // add reset class to reload button
  3011. switch(kind) {
  3012. case 'has_grid': scaleFonts(e,incr,id); scaleImages(e,incr,id); break; // scale grids
  3013. case 'has_font': scaleFonts(e,incr,id); break; // scale glyphs
  3014. case 'has_image': scaleImages(e,incr,id); break; // scale glyphs or images and fonts
  3015. }
  3016. }
  3017. function scaleFonts(e,scale,id) { scale = ( scale <= 0 ? (1, getEl('#font_size').value = 1 ) : scale ); let incr = Number(scale).toFixed(4); if ( id === 'decrease' ) { incr = 1/incr; scale = 1/scale } // ===> SCALE FONT
  3018. let content_grid = getEl('#content_grid'), font_specimen_viewer = getEl('#font_specimen_viewer'), font_file_glyph, font_file_grid = getEl('#font_file_grid'), value, font_input_value, data_scale, transform_scale, translateY, el_id;
  3019. let font_size = parseInt(getComputedStyle(document.body).fontSize).toFixed(4), fontGetSize = function(el) { return Number.parseFloat(window.getComputedStyle(el).fontSize).toFixed(4); };
  3020. switch(true) {
  3021. case hasContent('grid'): setStyle('#content_grid','font-size', ( fontGetSize(content_grid)/font_size * incr ) +'em'); break; // scale grid font items
  3022. // font files
  3023. case hasContent('font_file_glyph'): font_file_glyph = getEl('#font_file_glyph_viewer g'); // scale font file glyph
  3024. data_scale = font_file_glyph.style.transform; data_scale = Number( data_scale.match(/[\.\d]+/g)); if ( data_scale === 0 ) { data_scale = 1; } // define data_scale...
  3025. switch(true) {
  3026. case scale === null: data_scale = 1; font_input_value = 1; break;
  3027. case ( /increase|decrease/.test(id) ): data_scale = incr * data_scale; font_input_value = Math.pow(data_scale,0.2); break; // ...for +/- buttons; set #font-size value
  3028. default: data_scale = ( data_scale >= 1? Math.pow(incr,5) : Math.pow(incr,1.25) ); // ...for #font_size slider
  3029. } font_file_glyph.style.transform = 'scale('+ data_scale +')'; break; // scale glyph
  3030. case hasContent('font_file'):
  3031. if ( /increase|decrease/.test(id) ) {
  3032. scale = ( font_file_grid.dataset.scale === undefined ? scale : Number(font_file_grid.dataset.scale) <= 0 ? 1 : Number(font_file_grid.dataset.scale) + Number(scale) - 1 ); // scale
  3033. scale = Number(scale) * Number(incr); font_input_value = scale; // set slider value
  3034. }
  3035. transform_scale = ( scale >= 1? Math.pow(scale,4) : Math.pow(scale,1.125) ); translateY = ( Number(scale) < 0.5 ? 0.5 : Number(scale) < 1.5 ? (1 - scale) : -0.5 ); // transform_scale & translateY
  3036. font_file_grid.querySelectorAll('svg g').forEach(function(el) { el.style.transform = 'scale('+ transform_scale +') translateY('+ translateY +'em)'; }); // set transform scale & translateY
  3037. if ( scale > 0.33 ) {
  3038. font_file_grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(max(52px,'+ Math.pow(scale,3) * 1.33 +'em ), 1.5fr))'; // scale font file grid cols
  3039. font_file_grid.style.gridAutoRows = 'minmax(max(52px,'+ Math.pow(scale,3.33) * 1.33 +'em), max-content)'; // scale font file grid rows
  3040. }
  3041. font_file_grid.dataset.scale = scale; break;
  3042. // font specimens
  3043. case hasContent('font_specimen_glyph'): el_id = '#font_specimen_glyph'; // scale font specimen glyph (in vw)
  3044. switch(id) {
  3045. case 'font_size': // from font_size input slider
  3046. if ( incr === null ) { value = 64; } else { value = 64 * Math.pow(incr,3) } // max value of incr = 64 * 2^3 = 512 // reset if scale is 0
  3047. setStyle(el_id,'font-size', value +'vw'); getEl(el_id).dataset.scale = Number(value/64).toFixed(4); break;
  3048. default: // from increase/decrease buttons
  3049. getEl(el_id).dataset.scale = Number(getEl(el_id).dataset.scale) * incr; data_scale = Number(getEl(el_id).dataset.scale); // update data-scale; define data_scale
  3050. value = (64 * data_scale) +'vw'; setStyle(el_id,'font-size',value); font_input_value = Math.pow(data_scale,0.3333); // set the font-size using data-scale
  3051. } break;
  3052. case hasContent('font_specimen'): el_id = '#font_specimen_viewer';
  3053. switch(id) { // scale font specimen
  3054. case 'font_size': // from font_size input slider
  3055. if ( Number(incr) === Number(0.0000) ) { incr = 1; getEl('#'+id).value = 1; } // reset font size
  3056. setStyle(el_id,'font-size', ( Math.pow(incr,4) +'em' )); getEl(el_id).dataset.scale = Number(incr).toFixed(4); break; // set the font size
  3057. default: // from increase/decrease buttons
  3058. if ( Number(fontGetSize(font_specimen_viewer)) === 0 ) { incr = 1; } else { incr = Number(fontGetSize(font_specimen_viewer)/font_size * incr); } // reset if scale is 0
  3059. getEl(el_id).dataset.scale = incr.toFixed(4); setStyle(el_id,'font-size', incr.toFixed(4) +'em');
  3060. font_input_value = Math.pow(incr,0.25);
  3061. } break;
  3062. }
  3063. if ( (id === 'font_size' && scale === null) || id !== 'font_size' ) { getEl('#font_size').value = font_input_value } // set #font_size input value
  3064. scrollThis('#content_container','#content_font'); // scroll content_font
  3065. }
  3066. function scaleImages(e,incr,id) { // ===> ZOOM IMAGES ON CLICK
  3067. let content_el = ( elExists('#content_body') && elExists('#content_body > img') ? getEl('#content_body') : getEl('#content_container') );
  3068. let this_img = ( elExists('#content_body > img') ? getEl('#content_body > img') : getEl('#content_image') ); // define this_img
  3069. let CC_width = Math.round(content_el.offsetWidth), CC_height = Math.round(content_el.offsetHeight); // content_container dimensions
  3070. let img_width = Math.round(this_img.offsetWidth), img_height = Math.round(this_img.offsetHeight); // this_img dimensions
  3071. let CC_offset, img_offset, percentX, percentY, scrollX, scrollY;
  3072. const iframe_delta = ( getEls('#content_body > img').length === 1 ? Number.parseInt(getEl('#content_body').style.padding) : 0 );
  3073. switch(true) {
  3074. case hasContent('grid'): scaleImageGrid(incr,id); break; // scale grid images
  3075. default: // scale single images
  3076. imageGetDimensions( this_img.src, ( width,height ) => {
  3077. switch(true) {
  3078. case incr !== undefined && id !== undefined: // scale images by increment
  3079. addClass('#content_pane','has_scaled_image'); removeClass('#content_pane','has_zoom_image'); // remove zoom classes in case window resized after zoom
  3080. switch(true) {
  3081. case id === 'increase': this_img.style.cssText = `width:${img_width * incr}px; height:auto; max-width:none; max-height:none;`; break;
  3082. case id === 'decrease' && ( img_width >= 1 && img_height >= 1 ): this_img.style.cssText = `width:${img_width / incr}px; height:auto; max-width:none; max-height:none;`; break;
  3083. }
  3084. if ( Math.round(this_img.offsetWidth) >= CC_width ) { getEl('#content_image_container').scrollLeft = ( Math.round( this_img.offsetWidth ) - CC_width )/2; } // keep scaled img centered
  3085. switch(true) { // keep scaled img centered
  3086. case Math.round(this_img.offsetHeight) <= CC_height: getEl('#content_image_container').scrollTop = ( CC_height - Math.round( this_img.offsetHeight ) )/2; break;
  3087. default: getEl('#content_image_container').scrollTop = ( Math.round(this_img.offsetHeight) - CC_height )/2; break;
  3088. } break;
  3089. default: // else zoom single image on click
  3090. if ( width <= CC_width && height <= CC_height ) { // click to toggle small images between 100% and full size
  3091. if ( /100%/.test(this_img.getAttribute('style')) ) { this_img.removeAttribute('style'); } else { this_img.style.width = '100%'; }
  3092. removeClass('#content_pane','has_zoom_image has_scaled_image'); return; // no need to set scroll position
  3093. }
  3094. CC_offset = content_el.getBoundingClientRect(); img_offset = this_img.getBoundingClientRect(); // get offsets
  3095. percentX = Number((e.pageX - img_offset.left)/img_width).toFixed(2); percentY = Number((e.pageY - img_offset.top)/img_height).toFixed(2) // x,y coordinates of zoom click as % of image width/height
  3096. scrollX = (width * percentX) - e.pageX + CC_offset.left - (iframe_delta * width / img_width); // calculate clicked x-coordinates for full-size image
  3097. scrollY = (height * percentY) - e.pageY + CC_offset.top - (iframe_delta * height / img_height); // calculate clicked y-coordinate for full-size image
  3098. removeClass('#content_pane','has_scaled_image'); // in case image scaled already
  3099. getEl('#content_pane').classList.toggle('has_zoom_image');
  3100. getEl('#content_image_container').scrollTo(scrollX,scrollY); // scroll to clicked position
  3101. }
  3102. });
  3103. imageSetDimensions(); focusEl('#content_image_container'); // set image dimensions
  3104. }
  3105. }
  3106. function scaleImageGrid(incr,id) { // ===> SCALE IMAGE GRID ITEMS
  3107. if ( id === 'decrease' ) { incr = 1/incr; }
  3108. let grid_container = getEl('#content_grid'), grid_items = getEls('.image_grid_item img');
  3109. if ( !grid_items?.length ) { return; }
  3110. let grid_item_width = Number.parseFloat( grid_items[0].offsetWidth,10) * incr;
  3111. let grid_item_height = Number.parseFloat( grid_items[0].offsetHeight,10) * incr;
  3112. let grid_item_max_width = Number.parseFloat( grid_items[0].style.maxWidth,10) * incr;
  3113. let grid_item_max_height = Number.parseFloat( grid_items[0].style.maxHeight,10) * incr;
  3114. if ( grid_item_width < grid_item_max_width ) { grid_item_width = grid_item_max_width; } // don't reduce grid image size on first scale click
  3115. if ( grid_item_height < grid_item_max_height ) { grid_item_height = grid_item_max_height; } // don't reduce grid image size on first scale click
  3116. grid_container.style.gridTemplateColumns = 'repeat(auto-fill, minmax('+ (grid_item_width + 16) +'px, auto ) )'; // set grid properties
  3117. grid_items.forEach( (el) => { el.style.maxWidth = grid_item_width +'px'; el.style.maxHeight = grid_item_height +'px'; }); // set grid properties
  3118. return;
  3119. }
  3120. function imageGetDimensions(link, callback) { if ( link !== undefined ) { let img = new Image(); img.src = link; img.onload = function() { callback( this.width, this.height ); }; img = null; } } // GET IMG DIMENSIONS
  3121. function imageSetDimensions() { if ( !isTopWindow() ) { return; } // ===> SET IMAGE DIMENSIONS; // ignore iframe image
  3122. switch(true) {
  3123. case hasContent('image'):
  3124. imageGetDimensions( getAttr('#content_image','src'), function( width,height ) { // imageGetDimensions()
  3125. let percentage = (( getEl('#content_image').width/width ) * 100 ).toFixed(1); // define percentage
  3126. setAttr('#content_title span','data-after',' ('+ width +'px × '+ height +'px) ('+ percentage +'%)' ); // set dataset.after for content_title
  3127. }); break;
  3128. default: removeAttr('#content_title span','data-after'); // remove image dimensions
  3129. }
  3130. }
  3131. //============================// SHOW TEXT EDITOR
  3132. function showTextEditor(bool) { // ===> SHOW TEXT EDITOR; bool === false => hide editor
  3133. switch(true) {
  3134. case bool === false: case hasClass('body','has_texteditor'): removeClass('body','has_texteditor'); if ( elExists('.dirlist_item.selected') ) { null } else { focusEl('#top_body'); } break;
  3135. case !hasClass('body','has_texteditorUI'): buildTextEditorUI(); // no break; add the text editor UI if needed
  3136. default: addClass('body','has_texteditor'); getEl('#texteditor_raw_pane').focus(); // show editor: add class, focus texteditor
  3137. }
  3138. }
  3139. function tempHideTexteditor() { getEl('head').insertAdjacentHTML('beforeend','<style id="temp_styles">#text_container {display:none;}</style>'); } // ...hide editor UI for link files until formatted_link received from top
  3140. // to prevent FOUC for some kinds of content...
  3141. function showTexteditorPreview(content) { // ===> SHOW TEXTEDITOR PREVIEW
  3142. removeClass('#content_body','texteditor_view_raw texteditor_view_html'); // remove classes
  3143. addClass('#content_body','is_link texteditor_split_view_false texteditor_view_styled text_editing_enable_false'); // add classes to prevent editing without changing UI_Pref
  3144. getEl('#texteditor_styled_pane').innerHTML = content; // insert processed text from top
  3145. getEl('#temp_styles').remove(); // show texteditor after hiding it until processed content received
  3146. }
  3147. //============================// PDF
  3148. function showPDF() { // ===> SET UP CONTENT_PDF
  3149. let pdf_container = `<embed id="content_pdf" class="content_el position_relative border_0" tabindex="0" data-kind="pdf">`; // replace content_pdf for each pdf
  3150. getEl('#content_pdf').remove(); // remove existing content_pdf el
  3151. getEl('#content_image_container').insertAdjacentHTML('afterend',pdf_container); // add new content_pdf el
  3152. }
  3153. //============================// LINK FILES
  3154. function openLinkFile(e,id) { eStopPrevent(e); // ===> OPEN LINK FILES: on cmd-arrowdown or dblclick (webloc, url)
  3155. let link = getEl('#'+id).dataset.link; // get the link
  3156. switch(true) {
  3157. case link === undefined: break;
  3158. case !hasClass('#'+id,'non_local'): if ( !isTopWindow() ) { window.location = link; } else { showThis('open_link_file',false,true,[link,'link']); } break;
  3159. default: window.open(link); // else open link file links in new window
  3160. }
  3161. }
  3162. function linkFileProcess() { // ===> LINK FILE PROCESS: on "iframe_loaded" message received
  3163. let link_item = getEl('.dirlist_item.selected.link'), link_content = link_item?.dataset.html_content || '', regex = /URL\=(.+?)$|<key>URL<\/key>\s*<string\>(.+?)<\/string\>/im;
  3164. let link = ( link_content.match(regex)?.[1] || link_content.match(regex)?.[2] ), link_class = '', link_target = '', formatted_link; // get the link; define link elements
  3165. if ( window_protocol === 'file:' && !link.startsWith('file') ) { getEl('.dirlist_item.selected').classList.add('non_local'); link_class = ' class="non_local"'; link_target = ' target="_blank"'; }
  3166. if ( window_protocol !== 'file:' && link.startsWith('file') ) { getEl('.dirlist_item.selected').classList.add('local'); link_class = ' class="local"'; }
  3167. formatted_link = '<a id="link_file_01" href="'+ link +'"'+ link_class + link_target +'>'+ link +'</a>'; // assemble formatted link
  3168. link_item.dataset.link = link; // set data-link on sidebar item
  3169. link_item.dataset.html_content = null; // remove data-html_content
  3170. messageSend('iframe','show_texteditor_preview','',formatted_link); // send formatted link to iframe for display
  3171. }
  3172. //============================// DIRECTORY SOURCE
  3173. function showDirectorySource(link) { // ===> SHOW DIRECTORY SOURCE
  3174. switch(true) {
  3175. case hasClass('body','has_directory_source'): removeClass('body','has_directory_source'); showThis(getEl('.dirlist_item.non_media.selected').id); break; // close if open
  3176. default: addClass('body','has_directory_source'); showThis('show_directory_source',false,true,[(link || window_location)]); setAttr('#content_pane','data-loaded','loaded'); // else show directory source
  3177. }
  3178. }
  3179. function openSidebarInContentPane() { showThis('open_sidebar_in_content_pane',false,true,[window_location,'dir']); addClass('#top_body','open_sidebar_in_content_pane'); } // ===> OPEN SIDEBAR IN CONTENT PANE
  3180. function openInTextEditor() { // ===> OPEN IN TEXT EDITOR
  3181. let html = getData('.dirlist_item.htm.content_loaded','html_content');
  3182. showTextEditor(true);
  3183. if ( html !== undefined ) { getEl('#texteditor_raw_pane').value = html; getEl('#texteditor_styled_pane').value = html; getEl('#texteditor_html_pane').value = html; } // set previewed text
  3184. getEl('#open_in_texteditor')?.blur();
  3185. }
  3186. // END SHOW INDIVIDUAL CONTENT TYPES
  3187. //============================// AUTOLOAD CONTENT
  3188. function autoLoadItems() { // ===> AUTOLOAD FILE: index files or files from the file shortcut list
  3189. let selected_el = getEl('.dirlist_item.dir[id="rowid-'+ Number(getCurrentUIPref("selected")) +'"]');
  3190. switch(true) {
  3191. case ( selected_el !== null && isTopWindow() && searchParamsGet().has('selected') ): showThis(selected_el.id); break; // select from searchParam; prevents being overridden by autoload media
  3192. case ( getCurrentUIPref('autoload_index_files') !== 'false' && elExists('.dirlist_item.file.htm a[href*="/index."]') ): // load index file
  3193. showThis( getEl('.dirlist_item.file.htm a[href*="/index."]').closest('.dirlist_item').id ); break;
  3194. case selected_el !== null && selected_el.classList.contains('local'): case !isTopWindow(): break; // do nothing for local files or iframes
  3195. case searchParamsGet().has('file'): { // load files (from bookmark or url)
  3196. let file_name = decodeURIComponentSafe(getCurrentUIPref('file'));
  3197. let file = Array.from(getEls('.dirlist_item.file')).filter ( el => el.dataset.name === file_name );
  3198. if ( file[0] !== undefined ) { showThis( file[0].id ); }
  3199. searchParamDelete('file');
  3200. } break;
  3201. case hasClass('body','has_video'): getEl('video').volume = '0.5'; // video; set video volume
  3202. if ( getCurrentUIPref('media_autoload') === 'true' && !hasClass('body','has_audio') ) { showThis(getEl('.dirlist_item.video')?.id ); break; } // load video only if no audio
  3203. case hasClass('body','has_audio'): getEl('audio').volume = '0.5'; // audio; set audio volume
  3204. if ( getCurrentUIPref('media_autoload') === 'true' ) { showThis(getEl('.dirlist_item.audio')?.id ); } break; // load audio
  3205. case selected_el !== null: showThis( selected_el.id,false,true ); break;
  3206. }
  3207. if ( getEl('.content_el.has_content') === null ) { setData('#content_pane','content','has_null'); }
  3208. if ( selected_el !== null ) { scrollThis('#directory_list','.selected'); }
  3209. }
  3210. function getImageNames(id) { // ===> GET IMAGE NAMES (for cover art)
  3211. let images = getEls('.dirlist_item.image'), image_names = [], image_name, subdir_prefix;
  3212. if ( id.includes('_') ) {
  3213. subdir_prefix = RegExp("^"+id.slice(0,id.lastIndexOf('_') + 1),'m'); images = Array.from(images).filter( image => subdir_prefix.test(image.id) ); // filter images by subdir of selected item
  3214. }
  3215. for ( let image of images ) {
  3216. if ( image.id.split('_').length === id.split('_').length ) { // if audio and found image in same dir level...
  3217. image_name = image.dataset.name; image_name = image_name.slice(0,image_name.lastIndexOf('.') ); image_names.push( {'id':image.id,'name':image_name} ); // get image name w/o extension, add to image_names
  3218. }
  3219. }
  3220. return image_names;
  3221. }
  3222. function getCoverArtID(id) { // ===> GET COVER ART ID
  3223. let cover_art_id, match, exact_match, cover_names = ['cover','front','album','jacket','sleeve','cd','disc','insert','liner','notes'];
  3224. let selected_audio_name = ( getData('.file.audio.selected','name') || undefined ); // is there an image file with the same name?
  3225. if ( selected_audio_name !== undefined ) { selected_audio_name = selected_audio_name.slice(0,selected_audio_name.lastIndexOf('.')); cover_names.unshift(selected_audio_name); } // prep the name & add to cover names
  3226. const image_names = getImageNames(id); // get names of all image files
  3227. if ( image_names?.length === 0 ) { closeContent('image'); return; } // close existing image if no cover art found
  3228. for ( let cover_name of cover_names ) { // test available image names against cover names
  3229. exact_match = image_names.filter( el => el.name === cover_name ); match = image_names.filter( el => el.name.indexOf(cover_name) > -1 ); // check for exact and partial matches
  3230. switch(true) {
  3231. case exact_match.length > 0: return cover_art_id = exact_match[0].id; // if exact match, return
  3232. case match.length > 0: return cover_art_id = match[0].id; // else return first match
  3233. }
  3234. }
  3235. if (cover_art_id === undefined ) { return cover_art_id = image_names[0].id; } // if no matches, return first image id
  3236. }
  3237. function autoLoadCoverArt(bool,id) { // ===> AUTOLOAD COVER ART if dir contains audio & images
  3238. if ( bool === false || !isTopWindow() || !hasClass('body','has_images') || !hasClass('body','has_audio') ) { setData('#content_pane','content','has_null'); return; } // do nothing if no audio or images
  3239. let cover_ID = getCoverArtID(id), selected_ID = ( getCurrentUIPref('selected').length > 0 ? 'rowid-'+ getCurrentUIPref('selected') : undefined);
  3240. if ( cover_ID !== undefined ) {
  3241. showThis(cover_ID,true,true,[getEl('#'+cover_ID).querySelector('a').href,'autoloadcoverart']); removeClass('#'+cover_ID,'selected'); addClass('.dirlist_item.audio_loaded','selected')
  3242. } else { removeClass('.dirlist_item.image','content_loaded'); }
  3243. if ( selected_ID !== undefined && typeof selected_ID === 'number' ) { removeClass('.dirlist_item.media','selected'); addClass('#'+selected_ID,'selected'); }
  3244. }
  3245. //============================//
  3246. // ***** MAIN SHOW CONTENT FUNCTIONS ***** //
  3247. //============================//
  3248. // LINKS, SEARCH PARAMS, AND QUERIES
  3249. function makeSrcSearchParams(kind) { // ===> GET LINK QUERIES
  3250. let query_str = '', params;
  3251. const makeSearchParams = (params,query_str) => { query_str = new URLSearchParams(); for ( let param of params ) { query_str.append(param,getCurrentUIPref(param)); } return query_str; } // ===> MAKE SEARCH PARAMS
  3252. switch(true) {
  3253. case ( /audio|font|image|video|htm/.test(kind) ): break; // no query_str for audio, fonts, images, video, or htm
  3254. case kind === 'show_directory_source': query_str = '?&show_directory_source=true'; break; // view directory source
  3255. case kind === 'pdf': query_str = '#view=fitB&scrollbar=1&toolbar=1&navpanes=1'; break; // query_str for pdfs
  3256. case ( /text|markdown|code|other|link/.test(kind) ): // editable text files (including dot and plaintext files ["other"])
  3257. params = ['theme','text_editing_enable','texteditor_view','texteditor_split_view','texteditor_sync_scroll']; // define array of required params for text editing
  3258. query_str = '?'+ makeSearchParams(params).toString(); break; // compose query_str for text files
  3259. case ( /app|dir/.test(kind) ):
  3260. params = ['sort_by', 'sort_direction', 'show_details', 'show_image_thumbnails', 'show_image_thumbnails_always', 'show_large_image_thumbnails', 'show_numbers', 'use_custom_icons', 'show_invisibles', 'show_ignored_items', 'ignore_ignored_items', 'alternate_background', 'theme', 'media_play_all'];
  3261. if (searchParamsGet().has('ui_font')) {params.push('ui_font');} query_str = '?'+ makeSearchParams(params).toString(); break; // define array of params for dirs; compose query_str for dirs
  3262. }
  3263. return query_str; // return query_str
  3264. }
  3265. //============================// DIRS & SUBDIRS
  3266. function dirOpen(args) { // args: [dir.selected.id,dir.selected a.href] // ===> OPEN DIR (dirs only): update selected and history searchParam in querystring
  3267. const updateHistory = function(id) { let id_arr = id.split('_'); id_arr = id_arr.map(el => el.split('-')[1] ); return id_arr.reverse().join('+'); } // get numerical part(s) of selected dir/subdir id
  3268. let item = document.getElementById(args[0]), history = updateHistory(args[0]), searchParams = searchParamsGet(); // get selected item, history, and searchParams object
  3269. if ( item.classList.contains('dir') && item.classList.contains('invisible') && item.classList.contains('ignored') ) { return; } // don't attempt to open ignored invisible dirs (chiefly system dirs)
  3270. searchParams.delete('selected'); // delete 'selected' searchParam
  3271. history = ( searchParams.has('history') ? history +'+'+ searchParams.get('history') : history ); searchParams.set('history',history); // configure and set 'history' searchParam
  3272. window.location = args[1] +'?'+ searchParams.toString().replace(/%2B/g,'+'); // set the window location, replacing encoded + sign.
  3273. }
  3274. function subDirOpen(parent_id) { // ===> OPEN SUDIRECTORY
  3275. let parent_el = document.getElementById(parent_id), parent_link = parent_el.querySelector('a').href, level = Number(parent_el.dataset.level) + 1, body_id = document.body.id; // define subdir level
  3276. let content_iframe_utility_src = parent_link + makeSrcSearchParams('dir') + '&parent_id='+ parent_id +'&subdirectory=true&level='+ level +'&body_id='+ body_id; // assemble src link for utility_iframe
  3277. setAttr('#content_iframe_utility','src',content_iframe_utility_src); // set src for utility_iframe (which processes dir & sends it back to top)
  3278. parent_el.classList.add('dirlist_subdir_loading'); // removed when iframe_utility sends loaded message with subdir data
  3279. }
  3280. function subDirClose(subdir_id) { // ===> CLOSE SUBDIRECTOY
  3281. if ( !elExists('.dirlist_item.has_subdirectory') ) { return; }
  3282. let classes = [{ font:'has_fonts'},{image:'has_images'},{media:'has_media'},{audio:'has_audio'}]; // if no items with these classes found, remove the body class
  3283. removeClass('#'+subdir_id,'has_subdirectory');
  3284. getEls('.dirlist_item').forEach( (el) => { if (el.id.startsWith(subdir_id + '_')) { el.remove(); } }); // remove all items whose id begins with subdirectory parent id
  3285. classes.forEach ( (item) => { let key = Object.keys(item).toString(); if ( !elExists('#directory_list li.'+ key)) { removeClass('body',item[key].toString()) } });
  3286. if ( !elExists('.dirlist_item.selected') ) { getEl('#'+ subdir_id).classList.add('selected'); } // select closed dir if no other selected item exists
  3287. updateStats(false);
  3288. }
  3289. function subDirOpenClose(e,id) { if ( e !== null ) { eStopPrevent(e); e.stopImmediatePropagation(); } // ===> OPEN CLOSE SUBDIRECTORY; e === null when opening multiple subdirs
  3290. let parent_el = getEl('#'+id); removeClass('body','has_menu_stats');
  3291. switch(true) {
  3292. case ( parent_el.querySelector('.name') !== null && /\.trashes|\.temporaryitems|\.spotlight-v\d+/.test(parent_el.querySelector('.name').dataset.name ) ):
  3293. parent_el.classList.remove('dirlist_subdir_loading'); break;
  3294. default: if ( parent_el.classList.contains('has_subdirectory') ) { subDirClose(id); } else { subDirOpen(id); } // if ( e !== null ) { showThis(id,true,false); } // select parent dir // open/close the subdir
  3295. }
  3296. }
  3297. function subDirInsert(args) { // args = [items,classes,parent_id]; message received: add the subdirectory to the dir_list, update stats, check for additional selected dirs
  3298. let parent_item, source_el;
  3299. if ( !elExists('.dirlist_item.dirlist_subdir_loading') ) { return; } else { parent_item = document.getElementById(args[2]); } // select loading dir by id (args[2])
  3300. parent_item.classList.remove('dirlist_subdir_loading'); parent_item.classList.add('has_subdirectory'); // remove "loading" class, add "has_subdirectory" class
  3301. parent_item.insertAdjacentHTML('afterend',args[0]); // insert subdir items
  3302. if ( getCurrentUIPref('show_image_thumbnails') === 'true' ) { uiPrefImgThumbsToggle('true'); }
  3303. updateStats(true); // update stats
  3304. ['has_fonts','has_images','has_media','has_audio'].forEach( (subdir_class) => { if ( args[1].split(' ').includes(subdir_class) ) { addClass('body',subdir_class) } }); // add new body classes
  3305. initDirListEvents(); initIframeEvents(); // init dir_list event listeners
  3306. if ( elExists('.dir.selected:not(.has_subdirectory)') && parent_item.classList.contains('selected') ) { // open multiple selected subdirs, but don't open selected if current item is not selected
  3307. subDirOpenClose(null,getEl('.dir.selected:not(.has_subdirectory)').id); // i.e., allow unselected dirs to be opened by icon click without also opening selected dirs
  3308. }
  3309. if ( isTopWindow() ) { // reselect current content (audio or content_pane) if it was originally in newly-reopened subdirectory:
  3310. if ( /media/.test(args[1]) ) {
  3311. if ( !hasContent() ) { showThis(getEl('.dirlist_item.media[id^="'+ parent_item.id +'"]').id); } // autoload media from new subdirs, if no media or other content currently loaded
  3312. else if ( hasClass('#content_pane','has_audio') ) { addClass('#'+ getData('#audio','src_id'),'audio_loaded'); } // else reselect loaded audio file
  3313. }
  3314. if ( !hasContent('null') ) {
  3315. source_el = getEl('#'+ getData('#content_pane','loaded_id') ); // get content_el by content_source
  3316. if ( source_el !== null ) {
  3317. removeClass('.dirlist_item.non_media.selected,.dirlist_item.non_media.content_loaded','selected content_loaded'); source_el.classList.add('selected','content_loaded');
  3318. }
  3319. }
  3320. }
  3321. }
  3322. //============================//
  3323. // FOCUS ELS
  3324. function focusEl(sel,e) { let content_pane_data = getContentPaneData(), content_el_id = ( 'content'+ content_pane_data?.slice(3) || '' ), bool = false, incr = 1; // ===> FOCUS CONTENT
  3325. sel = ( sel === null || sel === undefined ? '#top_body' : !sel.startsWith('#') ? '#'+ sel : sel ); // enforce correct sel format
  3326. menuClose(); document.activeElement.blur(); // close menus, blur active element
  3327. switch(true) {
  3328. case sel === '#top_body': case sel === undefined: // focus sidebar
  3329. switch(true) {
  3330. case isTopWindow(): removeClass('body','focus_content'); getEl(sel).focus(); scrollThis('#directory_list','.selected'); break;
  3331. case !isTopWindow(): messageSend('top_body','focus_top'); break;
  3332. }
  3333. break;
  3334. case sel === '#content_iframe':
  3335. switch(true) {
  3336. case isTopWindow(): addClass('body','focus_content'); getEl('#content_iframe').focus();
  3337. if ( e !== undefined && e.key === 'Tab' ) {
  3338. if ( e.shiftKey ) { bool = true; incr = -1; }
  3339. switch(true) {
  3340. case hasContent('dir'): messageSend('iframe','iframe_navigation','',[e.key,bool]); break;
  3341. case hasContent('code,htm,markdown,text,other,link'): messageSend('iframe','navigateTabKeyFocus',null,['texteditor',incr]); break;
  3342. }
  3343. } break;
  3344. case !isTopWindow(): quickLookThis('close'); messageSend('top_body','focus_iframe'); break;
  3345. }
  3346. break;
  3347. case sel === '#content_pane': break; // <-- test and clean this focusEl\('#content_pane
  3348. case sel !== '#top_body': // All other sel
  3349. addClass('#top_body','focus_content'); removeClass('body','is_blurred');
  3350. switch(true) { // switch according to content_pane_data
  3351. case content_pane_data === undefined && isTopWindow(): removeClass('body','focus_content'); document.body.focus(); break; // don't focus undefined content
  3352. case hasContent('font'):
  3353. getEl(sel)?.focus();
  3354. if ( !hasContent('font_specimen_glyph') && !hasContent('font_file_glyph') ) { removeClass('#content_font .selected','selected'); }
  3355. switch(true) {
  3356. case sel === '#font_file_glyph_container_0': addClass(sel,'selected'); scrollThis('#font_file_grid','.selected'); getEl(sel)?.focus(); break;
  3357. case sel === '#font_specimen_glyph_container_0': addClass(sel,'selected'); scrollThis('#font_specimen_grid','.selected'); break;
  3358. }
  3359. switch(true) {
  3360. default: getEl(sel)?.focus(); break;
  3361. case sel === '#font_toolbar': getEl(sel)?.focus(); break;
  3362. case e?.key === 'Tab' || sel === '#font_specimen_1': getEl('#font_specimen_1')?.focus(); break;
  3363. } break;
  3364. case hasContent('grid,image,video,pdf'): document.getElementById(content_el_id).focus(); break;
  3365. default: getEl(sel)?.focus();
  3366. }
  3367. break;
  3368. }
  3369. }
  3370. //============================//
  3371. // SELECT DIR LIST ITEMS
  3372. function selectThisItem(id,bool) { let el = getEl('#'+id), kind = ( el?.dataset?.kind ?? null ); //, id_arr; // ===> SELECT THIS on click and set classes for content_pane; bool
  3373. switch(true) {
  3374. case id === 'close': case getEl('#'+id) || id === 'open_sidebar_in_content_pane': return;
  3375. //case ( /_/.test(id) && !isTopWindow() ): id_arr === id.split('_');
  3376. //id_arr.forEach( id => { subDirOpenClose(null,id); id_arr.shift(); } ); break; // we'd like to reopen iframe subdirs when iframe file/dir item from a subdir is closed
  3377. case el === null: break;
  3378. case kind === 'audio': removeClass('.audio','selected'); el.classList.add('selected'); if (!isTopWindow() ) { removeClass('.non_media','selected content_loaded'); } break; // audio; iframe dirlists
  3379. default: removeClass('body','has_directory_source'); addRemoveClassSiblings('#'+ id +':not(.audio)','selected content_loaded'); // select dir_list item; remove classes from siblings, leave .audio with .playing
  3380. }
  3381. if ( !/audio/.test(kind) && !/grid|/.test(id) ) { addRemoveClassSiblings('#'+ id,'content_loaded','content_loaded'); } // only remove siblings content_loaded class and change content_pane data-source if not audio
  3382. if ( /audio|video/.test(kind) && id !== undefined && isNaN(Number(getThisDuration(id))) ) { setThisDuration(id,0); getMediaDuration( getAttr('#'+id+' a','href'),kind,id ); } // reset media duration if necessary.
  3383. if ( /image/.test(kind) && getCurrentUIPref('show_image_thumbnails') === 'true' && getCurrentUIPref('show_image_thumbnails_always') === 'false' ) { loadImageThumbnail(id); } // load thumbnail if image_count > 2000
  3384. if ( bool !== false ) { scrollThis('#directory_list','.selected'); } // bool = false from autoloadcover art
  3385. }
  3386. function selectMultipleItems(e,id) { e.preventDefault(); // ===> SELECT MULTIPLE ITEMS
  3387. let el = ( id !== undefined ? getEl('#'+id) : null), els, kind = ( el?.dataset.kind || 'dir' );
  3388. if ( elExists('#content_grid div[data-id="'+id+'"]') ) { return; } // don't add items that are already in grid
  3389. switch(true) {
  3390. case e.key === 'a': e.preventDefault(); if ( isTopWindow() ) { closeContent(); } // cmd-A
  3391. kind = ( getEls('.dirlist_item.dir.selected,.dirlist_item.app.selected,.dirlist_item.image.selected,.dirlist_item.font.selected')[0]?.dataset.kind || 'dir');
  3392. els = getEls('.dirlist_item.'+kind); els?.forEach( el => el.classList.add('selected') );
  3393. if ( /image|font/.test(kind) ) { showGrid('show_'+kind+'_grid'); } break; // show grid of fonts/images
  3394. case ( /image|font/.test(kind) ) && !el.classList.contains('selected') && isTopWindow(): showGrid(id); break; // makes font/image grids
  3395. case ( /dir|app/.test(kind) ): removeClass('.dirlist_item.file.selected','selected'); removeClass('.dirlist_item.content_loaded','content_loaded'); el.classList.add('selected'); break; // select dirs
  3396. case ( /code|htm|markdown|text/.test(kind) ) && isTopWindow(): // text
  3397. removeClass('.dirlist_item.dir.selected','selected'); removeClass('.dirlist_item.dir.content_loaded','content_loaded'); // deselect dirs
  3398. el.classList.add('selected'); break; // select item
  3399. }
  3400. }
  3401. //============================//
  3402. // ===> SHOW CONTENT FUNCTIONS
  3403. function openFiles(e,id) { let funcName = id; // id/funcName === 'open_font' or 'open_playlist'
  3404. switch(e.type) {
  3405. case 'click':
  3406. switch(true) { // These cases are only for when a playlist or font file is already open (check for edited content_iframe...)
  3407. case (/has_\w+list/.test(getClassNames('body') )): e.preventDefault(); showWarning(funcName,'close_playlist'); break; // showWarning('openFile');
  3408. case hasContent('font_file'): e.preventDefault(); showWarning(funcName,'close_font'); break; // showWarning('openFile');
  3409. } break;
  3410. case 'change': openFile([e,id]); break;
  3411. }
  3412. }
  3413. // ===> SET CONTENT TITLE
  3414. function setContentTitle(id,kind,file_name,src,bool) { // ===> SET CONTENT TITLE
  3415. let title_text = '', selected_title = getData('.dirlist_item.selected','title'), selected_item = getEl('.dirlist_item.selected'), selected_link, content_link, content_link_info = getLinkInfo(src);
  3416. let title_span = ( kind === 'has_audio_error' ? getEl('#content_audio_title span') : getEl('#content_title span') );
  3417. switch(true) {
  3418. case id === 'close': removeAttr('#content_title span','data-after'); removeAttr('#content_title span','data-grid_count_items'); break;
  3419. default:
  3420. selected_link = ( selected_item !== null ? decodeURIComponentSafe( selected_item.querySelector('a').pathname ).trim() : '' ); // get selected item link
  3421. content_link = ( content_link_info !== undefined ? decodeURIComponentSafe( content_link_info[0] ).trim() : '' ); // get content link
  3422. switch(true) {
  3423. case kind === 'has_audio_error': title_text = "Audio file not found"; break; // audio error title
  3424. case kind === 'content_error': title_text = "File not found"; break; // content error title
  3425. case kind === 'grid': title_text = current_dir_path; break; // grid title
  3426. case selected_link !== content_link && bool !== false: // nav unsynced iframe items and...
  3427. try { title_text = decodeURIComponentSafe(content_link_info[8]) } catch(error) { title_text = content_link; } break; // ...error pages
  3428. default: title_text = file_name || selected_title; // nav synced iframe items
  3429. }
  3430. if ( kind !== 'grid' ) { title_text = title_text?.split('/').join('/<wbr>')?.split('_').join('_<wbr>'); removeAttr('#content_title span','data-after'); } // allow nice line breaks in title
  3431. if ( kind === 'image' ) { imageSetDimensions(); } // show images; set image dimensions
  3432. }
  3433. if ( title_span !== null ) { title_span.innerHTML = title_text; } // set title text
  3434. }
  3435. function showContentPaneEl(id) { setAttr('#content_pane','data-content','has_'+id); addClass('#content_'+id,'has_content'); getEl('#content_title span').innerHTML = getAttr('#content_pane','data-loaded_id'); }
  3436. function setContentPaneAttrs(id,kind,content_el_id) { // ===> SET CONTENT PANE ATTRIBUTES
  3437. let class_str_iframe = ( id === 'content_iframe_file' ? 'iframe_' : '' ), class_str_kind = ( /app|dir/.test(kind) ? 'dir' : 'file' ); // set strings to insert in content_pane class
  3438. switch(true) {
  3439. case id === 'close':
  3440. removeClass('#content_pane','content_error has_file has_dir has_zoom_image has_scaled_image has_emptycontent'); // removeclass various iframe_item_src
  3441. removeAttr('#content_pane .has_content','style'); removeAttr('#content_pane','data-loaded_id'); // remove inline styles and data-loaded_id
  3442. setAttr('#content_pane','data-content','has_null'); setAttr('#content_pane','data-loaded','loaded'); // set data-content=has_null, data-loaded=loaded (remove spinner)
  3443. break;
  3444. default:
  3445. switch(true) {
  3446. case ( /content_iframe_dir|content_iframe_parent|open_link_file/.test(id) ): removeClass('#content_pane','has_iframe_file'); addClass('#content_pane','has_iframe_dir'); break; // iframe_dirs
  3447. case ( /content_iframe_file/.test(id) ): removeClass('#content_pane','has_dir'); addClass('#content_pane','has_iframe_file'); break; // iframe files
  3448. case ( /^rowid/.test(id) ): default: removeClass('#content_pane','has_dir has_file has_iframe_dir has_iframe_file'); break; // all sidebar items
  3449. }
  3450. removeClass('#content_pane','content_error has_emptycontent'); // removeclass #content_pane error
  3451. addClass('#content_pane','has_'+ class_str_iframe + class_str_kind ); // addclass #content_pane "has_"+ kind
  3452. setAttr('#content_pane','data-content','has_'+ kind); // add data.content to content_pane
  3453. setAttr('#content_pane','data-loaded_id',id); // hide all iframe content until loaded, show loading spinner:
  3454. if ( kind !== 'video' ) { removeAttr('#content_video','data-src_id'); }
  3455. if ( /content_iframe/.test(content_el_id) && !/ignored/.test(kind) ) { setAttr('#content_pane','data-loaded','unloaded'); } else { setAttr('#content_pane','data-loaded','loaded'); }
  3456. }
  3457. }
  3458. function setContentElAttrs(id,content_el_id,kind,src,selected_id) { // ===> SET CONTENT EL ATTRIBUTES
  3459. switch(true) {
  3460. case ( /content_iframe_dir|content_iframe_parent|open_link_file/.test(id) ): // iframe_dirs
  3461. setAttr('#content_pane','data-iframe_item_src',src); // if iframe_dir, set iframe_item_src attr
  3462. if ( !hasAttr('#content_pane','data-iframe_selected_id') ) { setAttr('#content_pane','data-iframe_selected_id',selected_id); } break; // select iframe_dirlist selected if iframe_dir
  3463. case ( /content_iframe_file/.test(id) ): setAttr('#content_pane','data-iframe_selected_id',selected_id); break; // iframe files
  3464. case kind === 'ignored': return; // ignored
  3465. }
  3466. removeAttr('.content_el','src'); setAttr(content_el_id,'src',src); // set content el source
  3467. removeClass('.content_el.has_content','has_content'); addClass(content_el_id,'has_content'); // addclass .has_content to content_el & remove from siblings
  3468. if ( kind === 'image' ) { imageSetDimensions(); } // set image dimensions if necessary
  3469. }
  3470. function iframeLoadedFunctions(id,kind,file_name,content) { let focus_el; // ===> IFRAME LOADED FUNCTIONS
  3471. setAttr('#content_pane','data-loaded','loaded'); // set data-loaded (remove loading spinner)
  3472. if ( hasAttr('#content_pane','data-iframe_selected_id') ) { // select iframe_dirlist selected IFF is iframe_dir
  3473. messageSend('iframe','select_iframe_item','',getData('#content_pane','iframe_selected_id') ); // tell iframe to reselect original item
  3474. focus_el = '#content_iframe';
  3475. }
  3476. deleteData('.dirlist_item[data-html_content]','html_content'); // remove existing data-html_content
  3477. getEl('.dirlist_item.selected')?.classList.add('content_loaded'); // add content_loaded class to dirlist_item
  3478. getEl('.dirlist_item.content_loaded')?.setAttribute('data-html_content',content); // set data-html_content for sidebar items (for processing cue, playlists, link files, html, etc.)
  3479. switch (true) {
  3480. case file_name.endsWith('.cuetxt'): cuesheetProcess(content); break; // process cuesheet files; name must end with ".cuetxt"
  3481. case kind === 'link': linkFileProcess(); break; // process link files
  3482. case kind === 'htm' && content === '': addClass('#content_pane','has_emptycontent'); break; // if content == '', set empty class
  3483. case hasClass('#top_body','open_sidebar_in_content_pane'): addClass('#top_body','no_hover'); removeClass('#top_body','open_sidebar_in_content_pane'); focus_el = '#content_iframe'; break;
  3484. }
  3485. if ( focus_el !== undefined ) { focusEl(focus_el); } // focus element after iframe loaded
  3486. }
  3487. // ===> SHOW THIS ITEM // file_name = link_info[1], file_ext = link_info[2], kind = link_info[3], item_classes = link_info[4], body_classes = link_info[5], stats_classes = link_info[6];
  3488. function showThisItem(id,args) { // ===> SHOW CONTENT // args = [link,kind,selected_id (for iframe dirs/files)] or "close"; bool === false for proper content title for autoload_coverart
  3489. let link_info = ( /rowid/.test(id) ? ( getLinkInfo( getAttr('#'+ id +' a','href') ) ) : args !== undefined ? getLinkInfo(args[0]) : id );
  3490. let src, file_name, kind, content_el_id, selected_id = ( args?.[2] || undefined), content = ( args?.[4] || '' ), link_protocol = link_info?.[7];
  3491. let bool = (args?.[1] === 'autoloadcoverart' ? false : true); // bool: tell set_content_title to use name for cover art, not file path
  3492. if ( typeof link_info !== 'string' && link_info !== undefined ) { src = link_info[0]; file_name = link_info[1]; kind = ( link_info[4].includes('ignored') ? 'ignored' : link_info[3] ); } // src, file_name, kind
  3493. if ( !/editor/.test(id) ) { removeClass('body','has_texteditor'); } playlistShowItem('close'); // close texteditor and make_playlist item textarea
  3494. switch(true) { // make content src string
  3495. case id === 'close': id = 'close'; kind = 'close'; break; // close
  3496. case ( /\.php\?/.test(src) ): break; // do nothing for php files
  3497. case id === '': src = args[0]; kind = args[1]; break; // id = '' typically when grid items remain after closing subdirectory
  3498. case ( /open_sidebar_in_content_pane/.test(id) ): src = args[0] + makeSrcSearchParams('dir'); kind = args[1]; break; // open in sidebar
  3499. case ( /iframe_parent|iframe_dir/.test(id) ): src += makeSrcSearchParams('dir'); break; // prep for iframe dirs
  3500. case ( /rowid/.test(id) ): case ( /link/.test(kind) ): case id === 'content_iframe_file': src += makeSrcSearchParams(kind); break; // dirlist_items, link files, content_iframe_files
  3501. }
  3502. content_el_id = ( ['audio','font','image','pdf','video'].includes(kind) ? '#content_'+ kind : id === 'close' ? 'close' : '#content_iframe' );
  3503. switch(true) { // SHOW INDIVIDUAL CONTENT TYPES
  3504. case hasContent('font_file') && !/audio/.test(kind): showWarning('closeContent'); break; // prevent accidentally closing font file
  3505. case ( /audio/.test(kind) ): showMedia(kind,id,src); break; // show audio or audio_close
  3506. case ( /editor/.test(id) ): showTextEditor(); return; // show top text editor; don't change any other content pane params and attrs
  3507. case ( /iframe_loaded/.test(id) ): iframeLoadedFunctions(id,kind,file_name,content); break; // iframe_loaded
  3508. case ( /grid/.test(id) ): showGrid(id); break; // show grid
  3509. case ( window.location.protocol !== link_protocol ) && link_protocol !== undefined && !['audio','font','image','pdf','video','dir','app'].includes(kind): // replace this text with test has non_local class?
  3510. switch(true) {
  3511. case link_protocol !== 'file:': showWarning('non_local_file',[src]); break; // warning non-local file on local page (for playlist pages)
  3512. case link_protocol === 'file:': showWarning('local_file'); break; // warning local file on non-local page (for playlist pages)
  3513. } showThis('close'); break; // close previous content
  3514. case kind === 'ignored' && getCurrentUIPref('ignore_ignored_items') === 'false': window.location = src; break; // attempt to open ignored files if ignore ignored items enabled
  3515. default:
  3516. switch(true) {
  3517. case ( /autoloadcoverart/.test(kind) ): bool = false; break;
  3518. case ( /font/.test(kind) ): showFont(id,false,'',src); break; // show font specimen; init font preview event listeners
  3519. case ( /pdf/.test(kind) ): showPDF(); break; // show pdf: setup new #content_pdf el
  3520. case ( /video/.test(kind) ): if ( getData('#content_video','src_id') !== id ) { showMedia('video',id,src); break; } else { return; } // show video
  3521. }
  3522. if ( /font|image/.test(kind) && hasContent('grid') ) { closeGrid('hide'); }
  3523. if ( !hasContent('font_file_glyph') ) { setContentPaneAttrs(id,kind,content_el_id); setContentElAttrs(id,content_el_id,kind,src,selected_id); setContentTitle(id,kind,file_name,src,bool); initContentError(id,content_el_id); }
  3524. }
  3525. }
  3526. // ===> SHOW THIS // bool_1 !== false: select item; bool_2 !== false: show item
  3527. function showThis(id,bool_1,bool_2,args) { if (bool_1 !== false) { selectThisItem(id); } if (bool_2 !== false) { showThisItem(id,args); } } // ===> SHOW THIS (args = [src,kind,selected_id])
  3528. function quickLookThis(id,kind) { getEl('#audio')?.blur(); getEl('#content_video')?.blur(); // prevent media play on space key if focused
  3529. switch(true) {
  3530. case window.parent !== window.top: break; // prevent infinite quicklook regression
  3531. case id === 'close': closeContent(); removeClass('body','has_quicklook'); getEl('#content_pane .selected')?.scrollIntoView({behavior:"smooth",block:"nearest"}); break; // close; scroll grid item into view
  3532. default: addClass('body','has_quicklook'); showMedia('close_audio'); showThis(id); if ( /audio|video/.test(kind) ) { mediaPlayPause('play'); }
  3533. }
  3534. }
  3535. //============================//
  3536. // ===> CLOSE CONTENT
  3537. function closeContent(kind) { // Close all .content elements before opening any new .content from sidebar. // ===> CLOSE CONTENT
  3538. let content_el_id = ( getEl('.content_el.has_content')?.id || '' ); kind = ( kind || content_el_id.split('_')[1] );
  3539. switch(true) { // additional actions for specific cases
  3540. case hasContent('hidden_grid'): showGrid(); break; // show hidden grid
  3541. case kind === 'grid': case hasContent('grid'): closeGrid(); break; // close grid
  3542. case kind === 'font': case hasContent('font_file_glyph'): case hasContent('font_specimen_glyph'): case hasContent('font'): showWarning('close_font','close_font'); break; // close font specimen
  3543. // case kind === 'image': break; // case kind === 'audio': break; // case kind === 'video': break; // case kind === 'pdf': break; // case kind === 'iframe': break;
  3544. case kind === 'playlist': showWarning('closePlaylist'); break; // close playlist/filelist
  3545. case kind === 'texteditor': case hasClass('body','has_texteditor'): showTextEditor(false); break; // hide text editor
  3546. case hasContent('audio'): showMedia('close_audio'); break; // close audio if content pane empty
  3547. case hasClass('body','iframe_edited'): messageSend('iframe','unloading','closeContent'); break; // close edited iframe file with warning
  3548. case hasClass('body','has_directory_source') && !hasClass('body','is_error'): showDirectorySource(); break; // close directory source, reopen selected sidebar item
  3549. case kind === 'esc': removeClass('.selected,.content_loaded,.hovered,.is_blurred','selected content_loaded hovered,is_blurred'); removeClass('body','focus_content'); break;
  3550. default: showThis('close'); removeClass('body','focus_content has_directory_source'); break; //
  3551. case hasClass('#content_pane','has_iframe_file') && !hasClass('#content_pane','has_iframe_dir'): // if content has iframe file opened from sidebar dir
  3552. case !hasClass('#content_pane','has_iframe_file') && hasClass('#content_pane','has_iframe_dir'): // or if content has iframe dir...
  3553. removeClass('#content_pane','has_iframe_file'); // remove has_iframe_file class
  3554. showThis( getEl('.dirlist_item.non_media.selected')?.id ); break; // show the selected sidebar dir
  3555. case hasClass('#content_pane','has_iframe_file') && hasClass('#content_pane','has_iframe_dir'): // if content has iframe file from iframe dir...
  3556. removeClass('#content_pane','has_iframe_dir'); // remove has_iframe_dir class
  3557. showThis('content_iframe_dir',false,true,[getAttr('#content_pane','data-iframe_item_src'),'dir']); // show the iframe dir
  3558. removeAttr('#content_pane','data-iframe_item_src'); break;
  3559. }
  3560. }
  3561. //============================//
  3562. // ===> RESET CONTENT (Reset button or Cmd/Ctrl + R)
  3563. function resetContent() { let content_pane_data = getContentPaneData(); // ===> RELOAD CONTENT
  3564. switch(true) {
  3565. case !hasContent(): location.reload(); break; // reload window if no content visible
  3566. case hasContent('audio'): getEl('#audio').currentTime = 0; getEl('#audio').pause(); // nobreak; pause audio, reset time to 0
  3567. case hasContent('video'): getEl('#content_video').currentTime = 0; getEl('#content_video').pause(); break; // pause video, reset time to 0
  3568. }
  3569. switch(true) { // reset other content
  3570. case hasContent('texteditor'): case ( /has_\w+list/.test(getClassNames('#top_body')) ): break; // do nothing for audio, video, text editor, playlist content.
  3571. case hasContent('grid'): removeAttr('#content_grid,.image_grid_item img','style'); showGrid(); break; // reset grid
  3572. case hasContent('font'): fontReset('reset'); break;
  3573. case hasContent('image'): closeContent(); if ( elExists('.image.selected') || elExists('.image.content_loaded') ) { showThis(getEl('.dirlist_item.image.content_loaded').id); } break;
  3574. case hasContent('text,markdown,htm,iframe,dir'):
  3575. switch(true) {
  3576. case hasClass('body','iframe_edited'): messageSend('iframe','reloading','resetContent'); break;
  3577. default: showThis(getEl('.dirlist_item.content_loaded').id);
  3578. }
  3579. case hasContent('audio') || hasContent('video'): break; // don't do anything else for audio, video, text editor, playlist content.
  3580. case ( /has_ignored|undefined/.test( content_pane_data ) ): window.location = window.location.href; break; // reload page
  3581. }
  3582. deleteData('#content_pane','loaded'); // remove dataset.loaded in case file can't be read by utility iframe
  3583. }
  3584. //**********************// ===> NAVIGATION
  3585. function navigationGetType() { let content_pane_data = ( elExists('#content_pane') ? getContentPaneData() : 'iframe'), nav_type; // ===> GET NAVIGATION TYPE
  3586. switch(true) {
  3587. case hasClass('body','has_menu'): nav_type = ( getEl('#sidebar_menu_main .hovered') ? '#sidebar_menu_main .hovered ul' : '#sidebar_menu_main'); break; // submenu or menu
  3588. case content_pane_data === 'iframe' && elExists('#directory_list'): nav_type = '#directory_list'; break; // iframe dir_list
  3589. case ( hasContent('font_file_glyph') || hasContent('font_file') ) && hasClass('body','focus_content'): nav_type = '#font_file_grid'; break; // font file glyphs
  3590. case getEl('#font_specimen_grid').children.length > 0 && hasClass('body','focus_content') && document.activeElement.contentEditable !== true:
  3591. nav_type = '#font_specimen_grid'; break; // font specimen glyphs
  3592. case hasContent('image,font') && hasClass('#content_pane','has_hidden_grid') && hasClass('body','focus_content'):
  3593. case hasContent('grid') && hasClass('body','focus_content'): nav_type = '#content_grid'; break; // grids
  3594. default: nav_type = '#directory_list'; // default: dir_list
  3595. }
  3596. return nav_type; // = selector of container of items to be navigated
  3597. }
  3598. function navigateGetEl(args) { // ===> GET NEXT NAVIGATED ITEM
  3599. let els, els_length, selected_el_index, key = args[0], bool = args[1], nav_type = navigationGetType(), selected_el, selected_el_kind, navigated_el;
  3600. switch(true) { // Get selected_el
  3601. case bool === true: selected_el = getEl('.media.selected') || getEls('.audio_loaded,.media.content_loaded,.dirlist_item.is_blurred')[0] || getEl('.media'); break; // bool === true: autoplay media
  3602. default: selected_el = getEl(nav_type).querySelector('.selected'); // get currently selected item
  3603. }
  3604. if ( selected_el !== null ) {
  3605. selected_el.classList.remove('selected'); // If there is a selected item...remove its selected class unless shuffle
  3606. if ( !/warning_buttons/.test(nav_type) ) {
  3607. switch(true) { // get both images and fonts from mixed grids for L/R navigation...
  3608. case nav_type === '#content_grid' && elExists('.image_grid_item') && elExists('.font_grid_item'): selected_el_kind = new RegExp(/image|font/); break;
  3609. default: selected_el_kind = new RegExp(selected_el.dataset.kind); // or get selected_el kind (for L/R navigation)
  3610. }
  3611. }
  3612. if ( /audio|video/.test(selected_el_kind) && hasClass('body','media_play_all') ) { selected_el_kind = /audio|video/; } // but if media_play_all, get both media kinds
  3613. if ( selected_el?.classList.contains('hovered') && /ArrowUp|ArrowDown/.test(key) ) { getEl('#sidebar_menu_main .hovered')?.classList.remove('hovered'); }
  3614. }
  3615. els = Array.from(getEl(nav_type).children).filter( (el) => { // Get all navigable elements and filter
  3616. if ( /font_specimen_grid|font_file_grid|content_grid/.test(nav_type) || el.offsetWidth > 0 && el.offsetHeight > 0 ) { // only return visible items (or glyphs grid items)
  3617. if ( selected_el !== null && /ArrowLeft|ArrowRight/.test(key) && !/warning_buttons|menu/.test(nav_type) ) { // if L/R arrow and not menu or warning, and selected_el !== null...
  3618. return selected_el_kind.test(el.dataset.kind) && !el.classList.contains('unchecked'); // ...return all unchecked items of same kind as selected_el
  3619. } else {
  3620. return true; // else return all items
  3621. }
  3622. }
  3623. });
  3624. els_length = els.length; selected_el_index = ( selected_el === null ? -1 : els.indexOf(selected_el) ); // get index of selected item from filtered els or -1 if null
  3625. switch(true) { // GET NEXT NAVIGATED ELEMENT
  3626. case hasClass('body','has_directory_source'): // if viewing directory source, arrows will reopen selected sidebar item
  3627. if ( elExists('.dirlist_item.content_loaded') ) { showThis( getEl('.dirlist_item.content_loaded').id ); } else { showThis(selected_el.id); } return;
  3628. case key === 'Tab': if (selected_el === null ) { navigated_el = ( bool !== true ? els[0] : els[els_length - 1] ) } else { navigated_el = selected_el; } break; // navigation from tab into iframe
  3629. case key === 'ArrowUp': case key === 'ArrowLeft': // ArrowUp / ArrowDown
  3630. switch(true) {
  3631. case ( /audio|video/.test(selected_el_kind) && key === 'ArrowLeft' ):
  3632. switch(true) {
  3633. case hasClass('body','shuffle_media') && !/loaded/.test(selected_el.className): navigated_el = selected_el; mediaShuffleListUpdate(selected_el.id,false); break; // allow selected to be played next
  3634. default: navigated_el = navigateGetMediaLeftRightEl(els,selected_el,selected_el_index,key); // media
  3635. } break;
  3636. case ( /font_file|grid/.test(nav_type) && !hasClass('body','has_menu')): navigated_el = els[navigateGetGridItemIndex(selected_el_index,els_length,nav_type,key)]; break;
  3637. case ( hasClass('body','has_menu') && selected_el === null && key === 'ArrowLeft'): navigated_el = getEl('#sidebar_menu_parents'); break; // select parents menu if no main menu item selected
  3638. case ( selected_el?.classList.contains('is_submenu_item') && key === 'ArrowLeft' ): navigated_el = navigateGetSubMenuEl(selected_el,key); break; // go to parent menu
  3639. case ( selected_el === null || ( selected_el_index === 0 && !key === 'ArrowLeft' ) ): navigated_el = els[els_length - 1]; break; // select last if nothing selected
  3640. case ( selected_el_index === 0 ): navigated_el = els[els_length - 1]; break; // additional case for menus
  3641. default: navigated_el = els[selected_el_index - 1]; // default dir_list and menu items
  3642. } break;
  3643. case key === 'ArrowDown': case key === 'ArrowRight': // ArrowLeft / ArrowRight
  3644. switch(true) {
  3645. case ( /audio|video/.test(selected_el_kind) && key === 'ArrowRight' ):
  3646. switch(true) { // if autoplay off, navigated_el = selected_el, else navigated_el = next media item
  3647. case hasClass('body','shuffle_media') && !/loaded/.test(selected_el.className): mediaShuffleListUpdate(selected_el.id,false); // no break: allow selected item to be played next
  3648. case hasClass('body','media_autoplay_false') && bool === true: navigated_el = selected_el; break; // shuffle play
  3649. default: navigated_el = navigateGetMediaLeftRightEl(els,selected_el,selected_el_index,key);
  3650. } break;
  3651. case ( /font_file|grid/.test(nav_type) && !hasClass('body','has_menu') ): navigated_el = els[navigateGetGridItemIndex(selected_el_index,els_length,nav_type,key)]; break;
  3652. case ( selected_el === null || selected_el_index === els_length - 1 ): navigated_el = els[0]; break; // select first if nothing selected
  3653. case ( selected_el?.classList.contains('has_submenu') && key === 'ArrowRight' ): navigated_el = navigateGetSubMenuEl(selected_el,key); break; // open submenu
  3654. default: navigated_el = els[selected_el_index + 1]; // default dir_list and menu items
  3655. } break;
  3656. }
  3657. switch(true) { // WHAT TO DO WITH NAVIGATED ELEMENT:
  3658. case navigated_el === undefined:
  3659. navigated_el = ( /ArrowUp|ArrowLeft/.test(key) ? els[els_length - 1] : /ArrowDown|ArrowRight/.test(key) ? els[0] : null ); showThis(navigated_el?.id || navigated_el?.dataset.id); break;
  3660. case ( /grid|menu/.test(nav_type) ): removeClass('#sidebar_menu_main li','selected'); navigated_el?.classList.add('selected'); // for grids and menus; ...add selected class to navigated_el
  3661. switch(true) {
  3662. case hasClass('body','has_menu'): case hasClass('body','has_menu_parents'): break;
  3663. case navigated_el.id === 'sidebar_menu_parents': menuShow(null,'sidebar_menu_parents'); break;
  3664. case hasContent('font_specimen_glyph'): showFontGlyph(null,navigated_el.id); break;
  3665. case hasContent('font_file_glyph'): showFontGlyph(null,navigated_el.id); break; // show the navigated font file glyph
  3666. case hasClass('#content_pane','has_hidden_grid') && /image|font/.test(navigated_el.dataset.kind):
  3667. removeClass('.grid_item.selected','selected'); navigated_el?.classList.add('selected'); showThis(navigated_el.dataset.id); // no break
  3668. case ( /grid/.test(nav_type) ):
  3669. getEl('#directory_list .selected')?.scrollIntoView({block:"nearest"}); // scroll dir_list item into view
  3670. getEl('#content_pane .selected')?.scrollIntoView({behavior:"smooth",block:"nearest"}); break; // scroll grid item into view
  3671. } break;
  3672. case !isTopWindow():
  3673. switch(true) {
  3674. case hasClass('#content_body','has_quicklook'): quickLookThis(navigated_el.id,navigated_el.dataset.kind); break; // either quicklook or select item
  3675. default: showThis(navigated_el.id,true,false);
  3676. } break;
  3677. case ( /ArrowUp|ArrowDown/.test(key) && getData(navigated_el,'kind') === 'audio' && !hasClass('#content_body','has_quicklook') ):
  3678. showThis(navigated_el.id,true,false); break; // only select audio on U/D arrow
  3679. case ( /ArrowLeft|ArrowRight/.test(key) && navigated_el?.classList.contains('media') ):
  3680. switch(true) {
  3681. case hasClass('body','media_autoplay_false') && bool === 'true': showThis(selected_el.id,true,false); break;
  3682. default: showThis(navigated_el.id); // L/R arrow: load and play media
  3683. if ( selected_el_index === els_length - 1 && !hasClass('body','loop_media') && bool === true ) { null; } else { mediaPlayPause('play'); } // if last item & !loop, select first item, else play
  3684. } break;
  3685. default: showThis(navigated_el.id); // default: show item
  3686. }
  3687. }
  3688. function navigateGetSubMenuEl(selected_el,key) { let navigated_el; // ===> SUBMENU NAVIGATION
  3689. switch(true) {
  3690. case selected_el?.classList.contains('has_submenu') && key === 'ArrowRight':
  3691. selected_el.classList.add('hovered'); selected_el.querySelector('ul li.selected')?.classList.remove('selected');
  3692. navigated_el = selected_el.querySelector('ul li'); navigated_el.classList.add('selected'); break;
  3693. case selected_el?.classList.contains('is_submenu_item') && key === 'ArrowLeft':
  3694. removeClass('#sidebar_menu_main li.is_submenu_item.selected','selected');
  3695. navigated_el = getEl('#sidebar_menu_main .hovered'); navigated_el.classList.add('selected'); navigated_el.classList.remove('hovered'); break;
  3696. }
  3697. return navigated_el;
  3698. }
  3699. function navigateGetGridItemIndex(selected_el_index,els_length,nav_type,key) { let grid_col_count, grid_row_count, grid_item_index; // ===> GRID NAVIGATION
  3700. if ( /ArrowUp|ArrowDown/.test(key) ) { // calculate number of grid rows and columns
  3701. grid_col_count = ( Math.round( getEl( nav_type ).offsetWidth / getEl( nav_type +' > li').offsetWidth ) ); // number of grid items per row
  3702. grid_row_count = Math.floor(els_length / grid_col_count); // number of full grid rows
  3703. }
  3704. switch(true) {
  3705. case key === 'ArrowUp': // ArrowUp
  3706. switch(true) {
  3707. case selected_el_index === -1: grid_item_index = els_length - 1; break; // if nothing selected
  3708. case selected_el_index < grid_col_count: // if selected el is in first grid row...
  3709. switch(true) { // ...and if it is in a column to the right of last item in last row get...
  3710. case (grid_col_count * grid_row_count) + selected_el_index >= els_length: grid_item_index = selected_el_index + (grid_col_count * (grid_row_count - 1)); break; // last in penultimate col or...
  3711. default: grid_item_index = selected_el_index + (grid_col_count * grid_row_count); // last in last row
  3712. }
  3713. break;
  3714. default: grid_item_index = selected_el_index - grid_col_count; // default: grid_item_index = selected_el_index - length of grid row
  3715. } break;
  3716. case key === 'ArrowDown': // ArrowDown
  3717. switch(true) {
  3718. case selected_el_index === -1: grid_item_index = 0; break; // if nothing selected, get first item
  3719. case selected_el_index + 1 + grid_col_count > els_length: grid_item_index = ( selected_el_index - (grid_col_count * ( grid_row_count - 1)) ) % grid_col_count; break; // if selected is last in column
  3720. default: grid_item_index = Number(selected_el_index) + Number(grid_col_count); // default: index = selected_el_index plus the length of the grid row
  3721. } break;
  3722. case key === 'ArrowLeft': grid_item_index = ( ( selected_el_index === -1 || selected_el_index === 0 ) ? els_length - 1 : selected_el_index - 1 ); break; // if first or nothing selected, get last or prev
  3723. case key === 'ArrowRight': grid_item_index = ( ( selected_el_index === -1 || selected_el_index + 1 === els_length ) ? 0 : selected_el_index + 1 ); break; // if last or nothing selected, get first or next
  3724. }
  3725. if ( !hasContent('font_file') && nav_type !== '#font_specimen_grid' && nav_type !== '#font_file_grid' ) { // select corresponding dir_list item for image/font grids, but not font files
  3726. removeClass('.dirlist_item','selected hovered'); removeClass('.dirlist_item.content_loaded','content_loaded'); // remove classes from dir_list items
  3727. let selected_id = getEl(nav_type).querySelectorAll('.grid_item')?.[grid_item_index]?.dataset.id; // get the data-id from the currently selected grid item
  3728. if ( selected_id ) { getEl('#'+selected_id)?.classList.add('selected'); getEl('.dirlist_item.selected')?.scrollIntoView({behavior:"smooth",block:"nearest"}); } // select & scroll dir_list item into view
  3729. }
  3730. return grid_item_index;
  3731. }
  3732. function navigateGetMediaLeftRightEl(els,selected_el,selected_el_index,key) { let navigated_el, navigated_el_id; // ===> MEDIA LEFT/RIGHT NAVIGATION (Audio)
  3733. switch(true) {
  3734. case hasClass('body','shuffle_media'): navigated_el_id = mediaShuffleGetNextItem(); // if shuffle play enabled...get the next shuffled item id
  3735. switch(true) { // but if all shuffled items have been played (i.e., navigated_el_id === ''): if loop, update shufflelist, get next item; else get first item.
  3736. case navigated_el_id === '': navigated_el = ( hasClass('body','loop_media') ? ( mediaShuffleListUpdate(), navigated_el = getEl('#'+ mediaShuffleGetNextItem()) ) : navigated_el = els[0] ); break;
  3737. default: navigated_el = getEl('#'+ navigated_el_id ); // else get next item in the shufflelist
  3738. }
  3739. setData('#shuffle_label','shufflecount',' ('+ getAttr('#content_audio','data-shufflelist').split(',').length+' remaining)'); break;
  3740. case !selected_el.classList.contains('audio_loaded') && selected_el.classList.contains('audio') && isTopWindow(): navigated_el = selected_el; break; // if selected audio item not loaded, select it
  3741. case key === 'ArrowRight': navigated_el = ( selected_el_index + 1 < els.length ? els[selected_el_index + 1] : navigated_el = els[0] ); break; // if selected not last, select next, else select first
  3742. case key === 'ArrowLeft': navigated_el = ( selected_el_index - 1 !== -1 ? els[selected_el_index - 1] : navigated_el = els[els.length - 1] ); break; // if selected not first, select prev item, else last
  3743. }
  3744. selected_el.classList.remove('audio_loaded','content_loaded'); // deselect currently selected media item class
  3745. return navigated_el;
  3746. }
  3747. function navigateWarningBtns(e) { // ===> NAVIGATE WARNING BUTTONS
  3748. let buttons = getVisibleElsBySelector('#warning_buttons button');
  3749. let focused_button = getVisibleElsBySelector('#warning_buttons :focus,#warning_buttons .focus')[0], focused_btn_index = buttons.indexOf(focused_button);
  3750. removeClass('#warning_buttons button','focus');
  3751. switch(true) {
  3752. case e.shiftKey:
  3753. switch(true) {
  3754. case focused_button === null || buttons.indexOf(focused_button) === 0:
  3755. buttons[buttons.length - 1].focus(); buttons[buttons.length - 1].classList.add('focus'); break; // focus last button
  3756. default: buttons[focused_btn_index - 1].classList.add('focus'); buttons[focused_btn_index - 1].focus(); // else focus previous button
  3757. }
  3758. break;
  3759. default: // e.Tab
  3760. switch(true) {
  3761. case focused_button === null || buttons.indexOf(focused_button) === buttons.length - 1:
  3762. buttons[0].focus(); buttons[0].classList.add('focus'); break; // focus first button
  3763. default: buttons[focused_btn_index + 1].classList.add('focus'); buttons[focused_btn_index + 1].focus(); // else focus next button
  3764. }
  3765. }
  3766. }
  3767. function navigateByArrowModKey(e,id) { let args = [e.key], selected_el; // ===> ARROW KEY MODIFIER FUNCTIONS
  3768. if ( cmdAltKey(e) && ( /ArrowLeft|ArrowRight|ArrowUp|ArrowDown/.test(e.key) ) ) { return; } else { e.preventDefault(); } // prevents starting audio play when changing tabs; allows browser tab cycling
  3769. switch(true) {
  3770. case ( /ArrowUp|ArrowDown/.test(e.key) && e.shiftKey && !cmdKey(e) && !altKey(e) ): // e.shiftKey => select multiple items
  3771. let prev_el = getEl('#'+id).previousElementSibling, next_el = getEl('#'+id).nextElementSibling;
  3772. let sibling_id = ( e.key === 'ArrowUp' && prev_el !== null ? prev_el.id : e.key === 'ArrowDown' && next_el !== null ? next_el.id : null );
  3773. if ( sibling_id !== null ) { selectMultipleItems(e,sibling_id); } // if there is a sibling, select it
  3774. break;
  3775. case ( /ArrowLeft|ArrowRight/.test(e.key) && ( altKey(e) || altShiftKey(e) ) ): // alt/shift + L/R => mediaSkip(e)
  3776. if ( e.shiftKey ) { args.push(30); } else { args.push(10); } // scrub 10s or 30s
  3777. if (!isTopWindow()) { messageSend('top_body','mediaSkip','mediaSkip',args); } else { mediaSkip(e); }
  3778. break;
  3779. case cmdKey(e) && e.key === 'ArrowUp': // Cmd/Ctrl + Up
  3780. switch(true) {
  3781. case hasClass('body','is_dir'): case !isTopWindow(): iframeClick(e,'iframe_parent_link','link',getEl('#iframe_parent_link').href); break; // go to iframe parent
  3782. case isTopWindow() && hasClass('body','focus_content') && hasClass('#content_iframe','has_content'):
  3783. messageSend('iframe','open_iframe_parent_dir'); break; // fallback for go to iframe parent in case top is incorrectly focused
  3784. default: e.preventDefault(); showWarning('changeLocation',[getEl('#parent_dir_nav a').href,'false']); break; // go to parent (with warning for playlists/fonts/edited text)
  3785. } break;
  3786. case cmdKey(e) && e.key === 'ArrowDown': // Cmd/Ctrl + Down
  3787. switch(true) {
  3788. case !elExists('.selected'): break; // do nothing if nothing selected
  3789. case elExists('#content_body #directory_list') && cmdKey(e): iframeClick( e,id,'dblclick',getEl('#'+id).querySelector('a').href ); break;
  3790. case hasClass('.dirlist_item.selected','link'): openLinkFile(e,getEl('.dirlist_item.selected.link').id); break; // open webloc or url files
  3791. case hasClass('.dirlist_item.selected','playlist'): openPlaylist('','',getData('.dirlist_item.selected.playlist','playlist')); break; // open playlist or filelist
  3792. case isTopWindow() && hasClass('body','focus_content'): focusEl('#content_iframe',e); break; // select first item if nothing selected in iframe
  3793. case isTopWindow() && hasClass('.dirlist_item.selected','file') && !hasClass('.dirlist_item.selected','link'): break; // ? do nothing for link files
  3794. case isTopWindow() && hasClass('.dirlist_item.selected','dir') && hasClass('.dirlist_item.selected','app') && UI_Prefs_Bool.apps_as_dirs === false: break; // break if not viewing apps as dirs
  3795. default: showWarning('dirOpen', [getAttr('.dirlist_item.selected','id'), getAttr('.dirlist_item.selected a','href')]); // else double-click directories and all iframe items to open them
  3796. } break;
  3797. case cmdKey(e) && e.key === 'ArrowLeft': // Cmd/Ctrl + Left
  3798. switch(true) {
  3799. case elExists('.dirlist_item.dir.selected.has_subdirectory'):
  3800. getEls('.dirlist_item.dir.selected.has_subdirectory').forEach( dir => subDirClose(dir.id) ); break; // close all selected subdirectories
  3801. case !hasClass('.dirlist_item.selected','has_subdirectory') && elExists('.dirlist_item.has_subdirectory'): // if selected item is in subdirectory...
  3802. selected_el = getEl('.dirlist_item.selected');
  3803. if ( selected_el === null || selected_el.previousElementSibling === null ) { break; } // do nothing if no selection or prev element
  3804. while ( !selected_el.previousElementSibling.classList.contains('has_subdirectory') ) { selected_el = selected_el.previousElementSibling; } // find subdir "parent" === prev .has_subdirectory
  3805. if ( selected_el !== null && selected_el.previousElementSibling !== null ) { showThis( selected_el.previousElementSibling.id ); } break; // select and show parent dir
  3806. default: showThis( getVisibleElsBySelector('.dirlist_item')[0].id); // select first visible item
  3807. } break;
  3808. case cmdKey(e) && e.key === 'ArrowRight': try { subDirOpen(getEl('.dirlist_item.dir.selected:not(.has_subdirectory)').id); } catch(e) { null; } break; // Cmd/Ctrl + R: open (1st) selected subdirectory
  3809. case e.shiftKey: navigateGetEl([e.key,false,true]); break;
  3810. }
  3811. }
  3812. function navigateByArrowKey(args) { navigateGetEl(args); } // args[0] = key, args[1] = bool (for autoplay media),bool = shift // ===> ARROW KEY NAVIGATION
  3813. function arrowKeyFunctions(e,bool,el) { addClass('body','no_hover'); // 'e' = keyboardEvent or string (e.g. 'ArrowLeft/Right' from clickPrevNextButtons() // ===> ARROW KEY FUNCTIONS
  3814. if ( hasClass('body','has_help') ) { getEl('#help_container').focus(); return true; }
  3815. let id = ( /ArrowUp|ArrowLeft/.test(e.key) ? el?.querySelector('.selected')?.id : Array.from(el?.querySelectorAll('.selected'))?.reverse()[0]?.id ); // get first or last selected item
  3816. if ( !/texteditor/.test(document.activeElement.id) && !/textarea/.test(document.activeElement.tagName.toLowerCase() )) { window.getSelection().removeAllRanges(); document.activeElement.blur(); } // TEST all situations
  3817. switch(true) {
  3818. case ( /a|input|select|textarea/.test(document.activeElement.tagName.toLowerCase())) && !cmdKey(e): // nobreak: allow normal arrow key functions
  3819. case ( /texteditor/.test(document.activeElement.id)) && !cmdKey(e): // nobreak // " "
  3820. case document.activeElement.hasAttribute('contentEditable') && !cmdKey(e): // nobreak // " "
  3821. case !isTopWindow() && !elExists('#content_body #directory_list') && !cmdKey(e): return; // iframe is not a dir_list
  3822. case hasContent('pdf') && hasClass('body','focus_content'): focusEl('#content_pdf'); break; // focus content_pdf
  3823. case ( ( hasContent('zoom_image') || hasContent('scaled_image') ) && hasClass('body','focus_content') ): focusEl('#content_image'); return; // scroll imgs
  3824. case e.altKey: case e.ctrlKey: case e.metaKey: case e.shiftKey: navigateByArrowModKey(e,id); break; // arrow keys + modifiers
  3825. case isTopWindow() && hasClass('body','iframe_edited'): e.preventDefault(); messageSend('iframe','unloading','',['arrow_key_navigation',e.key]); break;
  3826. case isTopWindow() && hasClass('body','focus_content') && hasContent('dir'): e.preventDefault(); // req. after nav to iframe dir_list parent dir
  3827. switch(true) {
  3828. case hasClass('body','has_menu'): navigateByArrowKey([e.key,false]); break;
  3829. default: messageSend('iframe','iframe_navigation','',[e.key,false]); getEl('#content_iframe').focus();
  3830. } break;
  3831. case hasClass('body','has_top_menu') && !isTopWindow(): e.preventDefault(); messageSend('top_body','arrow_key_navigation','navigateByArrowKey',[e.key,false]); break; // menu navigation from focused iframe
  3832. case hasClass('body','has_quicklook'): e.preventDefault(); navigateByArrowKey([e.key,bool]); break;
  3833. default: e.preventDefault(); showWarning( 'navigateByArrowKey',[e.key,bool] ); // normal arrow key navigation, with warning
  3834. }
  3835. }
  3836. // NAVIGATION Go to Item
  3837. function goToItem(e) { e.stopPropagation(); e.preventDefault(); let value, input_el = getEl('#go_to_item input');
  3838. switch(true) {
  3839. case e.key === 'j' && cmdShiftKey(e): menuShow(e,'sidebar_menu_main_container'); // nobreak; show menu
  3840. case e?.type === 'click' && document.activeElement.id !== 'go_to_item_input': addClass('#go_to_item','show_input'); getEl('#go_to_item input').focus(); break; // show input on click
  3841. case e?.key === 'Enter': default:
  3842. value = input_el.value - 1; removeClass('#go_to_item','show_input'); input_el.value = null; menuClose(); showThis(getEls('#directory_list .dirlist_item')[value]?.id); break; // get the entered ui font
  3843. }
  3844. }
  3845. // NAVIGATION Tab Key
  3846. function navigateTabKeyFocus(kind,incr) {
  3847. let sel = { 'font':'#content_font .selected,#content_font *[data-tab_order],#font_toolbar *[data-tab_order]', 'grid':'#content_grid,#top_body', 'html':'a,button,input,select,textarea,div[contenteditable]', 'link':'#texteditor_styled_pane a', 'texteditor':'#content_texteditor .texteditor_pane' } // tab-able elements by nav type
  3848. let els = Array.from( getEls(sel[kind]) ).filter( el => el.offsetWidth > 0 && el.offsetHeight > 0 ), el_ids, next_item_id, font_els = []; // get tab-able elements; other vars
  3849. let active_el = (kind === 'font' ? getEls('#content_font .selected,#content_font *:focus,#font_toolbar *:focus')[0] : document.activeElement );
  3850. if ( els !== null ) { el_ids = ( kind !== 'html' ? els.map( el => el.id ) : els.map( el => els.indexOf(el) ) ) } else { return null } // get ids or define ids by index
  3851. if ( kind === 'font' && incr === 1 ) { // reorder font_specimen items
  3852. font_els = el_ids.filter(id => (/font_specimen/.test(id)) ); el_ids = el_ids.filter(id => (!/font_specimen/.test(id)) ); if ( incr === -1 ) { el_ids.reverse(); } el_ids = font_els.concat(el_ids);
  3853. if ( getEl('#font_file_grid .selected') ) { null; }
  3854. }
  3855. switch(true) { // get first or last item...
  3856. case active_el === undefined && kind === 'font' && incr === 1: case ( active_el?.tagName.toLowerCase() === 'body' || active_el === null ) && incr === 1: next_item_id = el_ids[0]; break;
  3857. case active_el === undefined && kind === 'font' && incr === -1: case ( active_el?.tagName.toLowerCase() === 'body' || active_el === null ) && incr === -1: next_item_id = el_ids[el_ids.length - 1]; break;
  3858. default: next_item_id = ( kind !== 'html' ? el_ids[el_ids.indexOf( active_el.id ) + incr] : el_ids[els.indexOf( active_el ) + incr] ); break; // or tab to next item for any other content
  3859. }
  3860. switch(true) {
  3861. case next_item_id === undefined: if ( kind === 'font' && !hasClass('body','focus_content') ) { addClass('body','focus_content'); } else { focusEl('#top_body'); } break; //focus top or font
  3862. case kind === 'grid': focusEl('#content_grid'); break;
  3863. case kind !== 'html': focusEl('#'+next_item_id); break;
  3864. case kind === 'html': if ( !getEl('#html_styles') ) { document.head.insertAdjacentHTML('beforeend','<style id="html_styles">'+ html_style_rules +'</style>'); } els[next_item_id].focus({focusVisible:true}); break; }
  3865. }
  3866. function navigateByTabKey(e,incr) { // ===> NAVIGATION TAB KEY
  3867. if ( e === null ) { null } else { incr = ( e.shiftKey ? -1 : 1 ); } // e === null if from message navigateTabKeyFocus;
  3868. let content_pane_data = getContentPaneData(), content_el_id = 'content'+ content_pane_data?.slice(3) || null, next_item_id;
  3869. switch(true) {
  3870. case hasClass('body','has_warning'): navigateWarningBtns(e); break; // nav warning buttons
  3871. case hasContent('texteditor'): navigateTabKeyFocus('texteditor',incr); break; // focus font preview, grid, text editor els
  3872. case hasContent('font,grid'): navigateTabKeyFocus(content_pane_data.slice(4),incr); break; // focus font preview, grid, text editor els
  3873. case !isTopWindow():
  3874. switch(true) {
  3875. case hasClass('body','is_dir'): messageSend('top_body','focus_top'); break;
  3876. case hasClass('body','is_link'): navigateTabKeyFocus('link',incr); break; // nav link file
  3877. case hasClass('body','is_html'): navigateTabKeyFocus('html',incr); break; // nav html file els (links, etc.)
  3878. case hasClass('body','is_text'): navigateTabKeyFocus('texteditor',incr); break; // nav html file els (links, etc.)
  3879. } break;
  3880. default:
  3881. switch(true) {
  3882. case hasContent('image,pdf,video') && !hasClass('body','focus_content'): next_item_id = content_el_id; break; // tab into images, pdf, video
  3883. case hasClass('body','has_texteditor') && !hasClass('body','focus_content'): // tab into text editor
  3884. case hasContent('dir,htm,markdown,text,code,other,link'): next_item_id = '#content_iframe'; break; // tab into iframe dirs and text files
  3885. case hasClass('body','focus_content'): next_item_id = '#top_body'; break; // tab into top
  3886. }
  3887. focusEl(next_item_id,e); // focusEl
  3888. }
  3889. }
  3890. // NAVIGATION Other
  3891. function navigateByTypedStr(e) { let items, item, timer; // ===> NAVIGATE BY TYPED STRING
  3892. switch(true) {
  3893. case ( /textarea|input/.test(document.activeElement.tagName.toLowerCase()) || document.activeElement.getAttribute('contentEditable') === true ): return; // ignore editable textareas
  3894. default:
  3895. timer = timeoutID(); if ( typeof timer === 'number' ) { window.clearTimeout( timer ); timer = 0; } timeoutID(); // set timer for typed string
  3896. str += e.key.toLowerCase(); // define typed string
  3897. switch(true) {
  3898. case hasClass('#top_body','has_menu'): // navigate main menu
  3899. items = ( getEls('#sidebar_menu_main > li.has_submenu.hovered').length > 0 ? getEls('#sidebar_menu_main > li.has_submenu.hovered li') : getEls('#sidebar_menu_main > li') ); // get menu/submenu items
  3900. items = Array.from(items).filter( item => item.innerText.toLowerCase().startsWith(str) ); // get menu items and filter items that match typed string
  3901. if ( items.length > 0 ) { item = items[0]; item?.parentNode.querySelector('.selected')?.classList.remove('selected'); item?.classList.add('selected'); } break; // select if matching menu item found
  3902. default: if ( elExists('.dirlist_item[data-name^="'+ str +'"]') ) { showThis(getEl('.dirlist_item[data-name^="'+ str +'"]').id); scrollThis('#directory_list','.selected'); } // select dir_list item
  3903. }
  3904. }
  3905. }
  3906. // ===> END NAVIGATION
  3907. //============================//
  3908. // ===> TEXT EDITING
  3909. function TextEditing() { // ===> TEXT EDITING Function: create Markdown Preview
  3910. let raw_markdown = ( elExists('#texteditor_raw_pane') ? getEl('#texteditor_raw_pane').value.toString() : '' );
  3911. MDmarkdown( raw_markdown, getEl('#texteditor_html_pane') );
  3912. MDsetChecklistClass(); // set checklist class in case any added
  3913. }
  3914. function textareaSelectContent(id) { let textarea_el = getEl('#'+id); focusEl('#'+ id); textarea_el.select(); textarea_el.scrollTop = 0; } // ===> SELECT TEXTAREA CONTENT
  3915. function texteditorClear() { // ===> CLEAR TEXT
  3916. if ( !isTopWindow() ) { messageSend('top_body','iframe_edited'); addClass('body','texteditor_edited'); } else { removeClass('body','texteditor_edited'); }
  3917. getEl('#texteditor_raw_pane').value = ''; getEl('#texteditor_styled_pane').innerHTML = ''; getEl('#texteditor_html_pane').value = '';
  3918. getEl('#texteditor_raw_pane').style.width = ''; focusEl('#texteditor_raw_pane');
  3919. removeAttr('#texteditor_styled_pane','srcdoc');
  3920. }
  3921. function texteditorSaveBtn(id) { let data, file_name, ext = ''; // let Text_Files = Text_Files.map( item => '.'+item ); // ===> SAVE BUTTON
  3922. switch(true) {
  3923. case hasContent('texteditor'): file_name = 'untitled'; break;
  3924. default: file_name = decodeURIComponentSafe(window.location.pathname.split('/').reverse()[0]);
  3925. }
  3926. switch(true) {
  3927. case id === 'save_text': data = getEl('#texteditor_raw_pane').value; break; // if ( Text_Files.() ) { ext = '.md'; } break;
  3928. case id === 'save_HTML': data = MDprepHTML( getEl('#texteditor_styled_pane').innerHTML ); ext = '.html'; break;
  3929. }
  3930. saveMD( data, file_name + ext );
  3931. }
  3932. function MDprepHTML(data) { // ===> MD PREP HTML for saving
  3933. let save_HTML_open = `<!DOCTYPE html><html><head><meta charset="utf-8" /><title></title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.min.css"></link> <style></style><script></script></head><body lang="en" class="markdown_body">`, save_HTML_close = '</body></html>';
  3934. data = data.replace(/<span\sclass="uplink">.<\/span>/g,'');
  3935. return save_HTML_open + data + save_HTML_close;
  3936. }
  3937. function saveMD(data,file_name) { // ===> SAVE MD
  3938. if ( !isTopWindow() ) { messageSend('top_body','save_text','',[data,file_name]); } else { saveFile(data,'text/plain',file_name); } // #top_body must save text, else a new window opens containing blob content
  3939. removeClass('body,#texteditor_raw_pane,#content_texteditor','texteditor_edited');
  3940. }
  3941. function MDmarkdown(raw_markdown) { // ===> MDMARKDOWN: Render markdown from processed source text
  3942. const MDcustomPreProcess = function(src) { return src; } // MD CUSTOM PREPROCESS (we're not doing anything here just yet...)
  3943. const MDcustomPostProcess = function(html) { // MD CUSTOM POSTPROCESS
  3944. html = html.replace(/<(p|li|dt|dd)>-*\s*\[\s*x\s*\]\s*(.+?)<\/(p|li|dt|dd)>$/gm,'<$1 class="checklist"><input type="checkbox" checked><label>$2</label></$3>') // checkboxes in p,li,dt,dd
  3945. .replace(/<(p|li|dt|dd)>-*\s*\[\s{1,}\]\s*(.+?)<\/(p|li|dt|dd)>$/gm,'<$1 class="checklist"><input type="checkbox"><label>$2</label></$3>') // checkboxes
  3946. .replace(/<li><p class="checklist">"/g,'<li class="checklist"><p>');
  3947. return html;
  3948. }
  3949. const MDit = window.markdownit( { linkify:false, typography:false, html:true } ).use( window.markdownitMultimdTable, { enableMultilineRows:true })
  3950. .use(window.markdownitCheckbox).use(window.markdownitSub).use(window.markdownitSup).use(window.markdownitFootnote).use(window.markdownitCentertext).use(window.markdownitDeflist).use(window.markdownitTocDoneRight);
  3951. let MD_Preview = MDcustomPostProcess( MDit.render( MDcustomPreProcess( raw_markdown ) ) );
  3952. let MD_script = `<style>body{margin:0;padding:0;}</style>`; // inline scripts to permit sync scrolling and focus
  3953. getEl('#texteditor_styled_pane').innerHTML = MD_script + MD_Preview; // set previewed text
  3954. let source_HTML = MD_Preview.toString();
  3955. getEl('#texteditor_html_pane').value = source_HTML; // set raw html view
  3956. }
  3957. function MDlivePreview() { MDmarkdown( getEl('#texteditor_raw_pane').value ); MDsetChecklistClass(); } // ===> MD LIVE PREVIEW
  3958. function MDliveCheckBoxes(checkbox,source_el,preview_el) { // ===> MD LIVE CHECKBOXES
  3959. const MDreplaceNthSubStr = function(str,substr,replacement,index) {
  3960. const MDreplaceAt = function(str, replacement, position) { str = str.substring(0, position) + replacement + str.substring(position + replacement.length); return str; }
  3961. let count = 0, found = substr.exec(str);
  3962. while ( found !== null ) { if ( count === index ) { return MDreplaceAt(str, replacement, found.index ); } else { count++; found = substr.exec(str); } }
  3963. }
  3964. removeClass('.checklist','clicked');
  3965. checkbox.closest('p,li,dt,dd').classList.add('clicked');
  3966. const this_index = preview_el.querySelector('.checklist').index( preview_el.querySelector('.checklist .clicked') );
  3967. const src_text = source_el.value;
  3968. const substr = new RegExp(/\[\s*.\s*\]/g);
  3969. const replacement = ( checkbox.is(':checked') ? '[x]' : '[ ]' );
  3970. source_el.value = MDreplaceNthSubStr(src_text, substr, replacement, this_index);
  3971. }
  3972. function MDsetChecklistClass() { getEls('#text_container input[type="checkbox"]').forEach( el => el.closest('ul').style.cssText = 'list-style:none;padding:0;' ); } // ===> MD SET CHECKBOX LIST CLASS
  3973. function texteditor_ResizeSplit() { // ===> MD RESIZE SPLIT VIEW
  3974. let page_width = window.innerWidth, editor_width = getEl('#content_texteditor').offsetWidth, editor_offsetLeft = ( document.body.id === 'top_body' ? getEl('#content_pane').offsetLeft : 0);
  3975. document.onmousemove = function(e) { eStopPrevent(e);
  3976. let pageX = e.pageX;
  3977. if ( pageX > editor_offsetLeft + 150 && pageX < page_width - 150 ) { // min split pane widths
  3978. setStyle('#text_editing_handle','left', pageX - editor_offsetLeft - 4 + 'px');
  3979. setStyle('#texteditor_raw_pane','width', pageX - editor_offsetLeft + 'px');
  3980. setStyle('#texteditor_styled_pane','width', editor_width + editor_offsetLeft - pageX + 'px');
  3981. setStyle('#texteditor_html_pane','width', editor_width + editor_offsetLeft - pageX + 'px');
  3982. }
  3983. }
  3984. }
  3985. function texteditor_ResetSplit() { getEls('#text_container .texteditor_pane,#text_editing_handle').forEach( el => el.removeAttribute('style') ); }
  3986. function texteditor_SyncScroll(e) { // ===> MD SYNC SCROLL
  3987. if ( !getEl('input[name="texteditor_sync_scroll"').checked || hasClass('body','texteditor_split_view_false') || hasClass('body','text_editing_enable_false') ) { return; } // ignore if no split or no sync scroll
  3988. let scrolled = e.currentTarget, scrolled_scrollTop = scrolled.scrollTop, scrolled_height = scrolled.scrollHeight,
  3989. scrolled_percentage = (scrolled_scrollTop/scrolled_height).toFixed(4);
  3990. let synced_id = ['texteditor_raw_pane','texteditor_styled_pane','texteditor_html_pane'].filter(el => el !== scrolled.id).filter(el => document.getElementById(el).offsetHeight > 0).toString();
  3991. let synced = document.getElementById(synced_id); // the element to be sync scrolled
  3992. synced.scrollTo(0, (scrolled_percentage * synced.scrollHeight).toFixed(0), {behavior:'smooth'});
  3993. }
  3994. function MDtocClick(id) { let thisId = getEl('#'+id).href; if ( thisId !== null ) { getEl('#texteditor_styled_pane').scrollTop = getEl('#'+id).offset().top - 48; } } // ===> MD TOC CLICK anchors
  3995. function MDheaderClick() { // ===> MD HEADER CLICK
  3996. switch(true) {
  3997. case elExists('#texteditor_styled_pane .table-of-contents'): getEl('#texteditor_styled_pane').getElementsByClassName('table-of-contents')[0].scrollIntoView({behavior:"smooth",block:"nearest"}); break;
  3998. default: getEl('#texteditor_styled_pane').scroll(0,0);
  3999. }
  4000. }
  4001. //***********************//
  4002. // MESSAGES
  4003. function messageSend(target,message,funcName,args) { // ===> SEND MESSAGE to iframe or parent
  4004. let messageObj = { 'messageContent': message, 'functionName': funcName, 'arguments': args };
  4005. switch(target) {
  4006. case 'iframe': getEl('#content_iframe').contentWindow.postMessage( messageObj, '*' ); break;
  4007. case 'top_body': window.parent.postMessage( messageObj,'*'); break;
  4008. }
  4009. }
  4010. function messageReceive(e) { // ===> RECEIVE MESSAGE from iframe or parent, do appropriate action
  4011. if ( e.data.messageContent === 'iframe_loaded' ) { showThis('iframe_loaded',false,true,e.data.arguments); }
  4012. else if ( e.origin === 'null' || e.origin === origin ) { let message = e.data.messageContent, args = e.data.arguments;
  4013. switch( message ) {
  4014. case 'uiPrefToggle': uiPrefToggle(args); break;
  4015. case 'searchParamSet': searchParamSet(args[0],args[1]); break;
  4016. case 'set_ui_font': if ( args !== '' ) { document.body.style.fontFamily = args; } else { document.body.style.fontFamily = null; } break; // set iframe ui font
  4017. case 'arrow_key_navigation': removeClass('body','iframe_edited'); navigateByArrowKey(args); break; // class_name, key
  4018. case 'iframe_navigation': addClass('body','no_hover'); navigateByArrowKey(args); break; // get first or last iframe dirlist item
  4019. case 'show_sidebar': document.body.classList.toggle(getNewUIPref('show_sidebar')[0]); break;
  4020. case 'toggle_menu': menuShow(null,'sidebar_menu_main_container'); messageSend('iframe','has_top_menu'); break; // show menu, tell iframe to allow menu arrow navigation
  4021. case 'menu_close': menuClose(); break;
  4022. case 'has_top_menu': addClass('#content_body','has_top_menu is_blurred'); break; // tell iframe top has menu to allow arrow navigation
  4023. case 'menu_selection': case 'menuClick': menuClick(); break; // show menu
  4024. case 'menu_navigation': navigateByArrowKey(args); break; // menu navigation from iframe
  4025. case 'toggle_invisibles': getEl('#show_invisibles input').click(); getEl('#show_invisibles input').blur(); break;
  4026. case 'blur_top': addClass('#top_body','focus_content'); menuClose(); break;
  4027. case 'focus_top': // close menus and refocus content or focus sidebar
  4028. switch(true) {
  4029. case hasClass('#top_body','focus_content') && hasClass('#top_body','has_menu'): focusEl('#content_pane'); break;
  4030. default: focusEl('#top_body'); break;
  4031. } break;
  4032. case 'focus_iframe': addClass('body','focus_content'); break; // focusEl('#content_iframe'); break;
  4033. case 'theme_light': case 'theme_dark': // toggle iframe UI theme and iframe Text Editor theme
  4034. getEl('#content_body').classList.remove('theme_dark','theme_light');
  4035. getEl('#content_body').classList.add(message,'texteditor_'+ message); break; // change iframe dir theme
  4036. case 'show_iframe_parent': showThis('content_iframe_parent',false,true,args); break; // args[0] === item link, args[1] === item kind
  4037. case 'show_iframe_dir': showThis('content_iframe_dir',false,true,args); break; // args[0] === item link, args[1] === item kind
  4038. case 'show_iframe_file': showThis('content_iframe_file',false,true,args); break; // args[0] === item link, args[1] === item kind
  4039. case 'select_iframe_item': getEl('#'+args)?.classList.add('selected'); scrollThis('#directory_list','.selected',false); break;
  4040. case 'open_iframe_dir_in_sidebar': window.location = args; break; // tell top to open iframe directory in sidebar; args: iframe dir url
  4041. case 'open_iframe_parent_dir': iframeClick(e,'parent','link',getAttr('#iframe_parent_link','href')); break; // getEl('##parent').find('a').click();
  4042. case 'close': clickThis('#close_btn'); break; // escape content_iframe and close content
  4043. case 'close_content': showThis('close'); removeClass('body','iframe_edited'); focusEl('#top_body'); break; // close edited_iframe text after clicking "Save/Don't Save" buttons
  4044. case 'reload': showWarning('resetContent'); break; // reload content
  4045. case 'resetContent': showThis(getEl('.dirlist_item.content_loaded').id); removeClass('body','iframe_edited'); break; // reload iframe content after "Save/Don't Save" buttons
  4046. case 'showThis': removeClass('body','iframe_edited'); focusEl('#top_body'); showThis(args); break; // show clicked/navigated sidebar item after "Save/Don't Save" buttons
  4047. case 'show_numbers': case 'show_invisibles': case 'alternate_background': case 'show_ignored_items': case 'ignore_ignored_items':
  4048. getEl('#content_body').classList.toggle(message); break; // toggle iframe dir_list UI prefs from main menu:
  4049. case 'show_image_thumbnails': uiPrefImgThumbsToggle(); break; // toggle image thumbnails in iframe
  4050. case 'iframe_play_pause_media': mediaPlayPause(); break; // tell top to play/pause audio from iframe click
  4051. case 'mediaSkip': mediaSkip(undefined,args); break; // tell top to mediaskip from focused iframe
  4052. case 'play_prev_next_iframe_audio': mediaPlayPrevNextIframeItem(args); break; // play next iframe track
  4053. case 'close_iframe_audio': removeClass('.audio_loaded','audio_loaded'); break; // remove iframe audio loaded class
  4054. case 'set_media_duration': setMediaDuration(args[0],args[1],args[2],true); break; // set media durations for subdirs [id, item_sort_kind, duration]
  4055. case 'refresh_media_durations': refreshMediaDurations('refresh_media_durations'); break;
  4056. case 'navigateTabKeyFocus': navigateTabKeyFocus(args[0],args[1]); break; // args[0] = kind, args[1] = incr (tab:1,shift+tab:-1)
  4057. case 'texteditor_split_view': uiPrefToggle('texteditor_split_view'); break;
  4058. case 'iframe_edited': if ( !hasClass('#top_body','iframe_edited') ) { addClass('#top_body','iframe_edited'); } break; // let top know iframe text has been edited
  4059. case 'texteditor_toolbar_button': if ( !isTopWindow() ) { document.body.classList.toggle(args); } else { uiPrefToggle(args); } break;
  4060. case 'clear': addClass('#top_body','iframe_edited'); break; // add edited class after clearing text from edited iframe file
  4061. case 'save_text': removeClass('body','iframe_edited'); saveFile(args[0],'text/plain',args[1]); break;
  4062. case 'iframe_text_saved': removeClass('body','iframe_edited'); break;
  4063. case 'toggle_texteditor': showTextEditor(true); break;
  4064. case 'unloading': showWarning('closeContent',args); break; // show unsaved changes warning in iframe
  4065. case 'reloading': showWarning('resetContent'); break;
  4066. case 'iframe_loaded': showThis('iframe_loaded',false,true,args); break; // args = [iframe_src,file_name,kind,content]
  4067. case 'dirlist_subdir_loaded': subDirInsert(args); break; // when subdirs processed, insert subdirs in dirlist
  4068. case 'show_texteditor_preview': showTexteditorPreview(args); break; // only show previewed text for certain files (e.g., webloc, url)
  4069. case 'local_link': showWarning('warning_local_file'); break; // local link warning
  4070. case 'iframe_playlist': getEls('.dirlist_item.text').forEach( el => el.removeAttribute('data-playlist')); // iframe_playlist
  4071. removeClass('.dirlist_item.text','playlist'); addClass('.dirlist_item.text.selected','playlist'); setData('.dirlist_item.text.selected','playlist',args); break;
  4072. case 'setIframePlayerStatus': // for iframe audio playback
  4073. if ( args === 'play' ) { removeClass('body','is_paused'); addClass('body','is_playing'); } else { removeClass('body','is_playing'); addClass('body','is_paused'); } break;
  4074. }
  4075. }
  4076. }
  4077.  
  4078. // END MESSAGES
  4079. //============================//
  4080. // WARNINGS
  4081. function doFunction(funcName,args) { // ===> DO FUNCTION
  4082. let funcDictionary = { 'navigateByArrowKey':navigateByArrowKey, 'showThis':showThis, 'dirOpen':dirOpen, 'null':null, 'menuClick':menuClick, 'clickThis':clickThis, 'texteditorClear':texteditorClear, 'closeContent':closeContent, 'showFont':showFont, 'showFontGlyph':showFontGlyph, 'mediaSkip':mediaSkip, 'closePlaylist':closePlaylist, 'openSidebarInContentPane':openSidebarInContentPane, 'resetContent':resetContent, 'setLocation':setLocation, 'showDirectorySource':showDirectorySource, 'uiPrefToggle':uiPrefToggle, 'openInTextEditor':openInTextEditor, 'playlistMake':playlistMake,'changeLocation':changeLocation }; // list of functions to remember and execute after warning button click
  4083. return funcName === 'null' ? null : funcDictionary[funcName](args); // return the function and call it with args
  4084. }
  4085. function showWarning(funcName,args) { // ===> SHOW WARNING
  4086. switch(true) {
  4087. case funcName === null: break;
  4088. case ( /warning_make_playlist/.test(funcName) ):
  4089. getEls('#directories_only,#files_only,#audio_files_only,#video_files_only,#media_files_only,#all_non_media_files').forEach(el => el.removeAttribute('disabled')); // setup makeplaylist alert
  4090. if ( getEl('.dirlist_item.dir') === null ) { getEl('#directories_only').disabled = 'disabled'; } if ( getEl('.dirlist_item.file') === null ) { getEl('#files_only').disabled = 'disabled'; }
  4091. if ( !hasClass('body','has_audio') ) { getEl('#audio_files_only').disabled = 'disabled'; } if ( !hasClass('body','has_video') ) { getEl('#video_files_only').disabled = 'disabled'; }
  4092. if ( !hasClass('body','has_media') ) { getEl('#media_files_only').disabled = 'disabled'; } if ( getEl('.dirlist_item.non_media') === null ) { getEl('#all_non_media_files').disabled = 'disabled'; }
  4093. openWarning('warning_make_playlist',['warning_btn_ok','warning_btn_cancel'],'playlistMake',args); break; // make playlist/filelist
  4094. case ( /open_font|open_playlist|close_font|closePlaylist/.test(funcName) ): // close playlist or font file; args === close_font, close_playlist; if funcName[close_] === args, close item, else open file
  4095. if ( !hasContent('font_file_glyph') && !hasContent('font_specimen') && !hasContent('font_specimen_glyph') && !hasClass('body','has_quicklook') )
  4096. { openWarning('warning_'+args,['warning_btn_cancel','warning_btn_ok'],funcName,args); } else { closeFont(); /*i.e., glyphs*/ } break; // close font or font glyphs
  4097. case ( /texteditorClear/.test(funcName) ): openWarning('warning_clear_text',['warning_btn_ok','warning_btn_save','warning_btn_cancel'],funcName); break;
  4098. case ( /non_local_file/.test(funcName) ): openWarning('warning_non_local_file',['warning_btn_ok','warning_btn_cancel'],null,args); break;
  4099. case ( /local_file/.test(funcName) ): openWarning('warning_local_file',['warning_btn_ok']); break;
  4100. case ( /showThis|closeContent|resetContent|changeLocation|setLocation/.test(funcName) ): // warnings for various cases
  4101. switch(true) {
  4102. case !isTopWindow():
  4103. switch(true) {
  4104. case args?.length === 2: openWarning('unloading',['warning_btn_save','warning_btn_dont_save','warning_btn_cancel'],args[0],args[1]); break; // iframe edited warning for dirlist_item click
  4105. default: openWarning('unloading',['warning_btn_save','warning_btn_dont_save','warning_btn_cancel'],funcName,args); // iframe unload warning for close/resetContent
  4106. } break;
  4107. default: doFunction(funcName,[args]); break; // default: perform requested function w/o warning
  4108. }
  4109. break;
  4110. default:
  4111. switch(true) {
  4112. case hasClass('body','iframe_edited'): messageSend('iframe','unloading',funcName,args); break; // send unloading message for close or resetContent
  4113. case hasClass('body','texteditor_edited') && !/navigateByArrowKey|uiPrefToggle/.test(funcName): case funcName === 'texteditorClear':
  4114. if ( isTopWindow() ) { removeClass('#content_pane','has_hidden_texteditor'); setData('#content_pane','content','has_texteditor'); }
  4115. openWarning('texteditorClear',['warning_btn_save','warning_btn_dont_save','warning_btn_cancel']); break;
  4116. default: doFunction(funcName,args); break;
  4117. }
  4118. }
  4119. }
  4120. function openWarning(id,buttonids,funcName,args) { // ===> OPEN WARNING
  4121. addClass('body','has_warning'); removeAttr('#warnings_container','class'); addClass('#warnings_container',id);
  4122. addClass('#'+id,'show'); buttonids.forEach( el => addClass('#'+el,'show') ); // show warning buttons and message
  4123. getEl('#'+ buttonids[0]).classList.add('focus'); // focus default warning button
  4124. if ( funcName !== undefined ) { setData('#warnings_container','funcname',funcName); } // store funcName and args to complete after clicking warning button
  4125. if ( args !== undefined ) { setData('#warnings_container','args',args); }
  4126. }
  4127. function closeWarning() { // ===> CLOSE WARNINGS
  4128. removeClass('body','has_warning'); getEls('#warnings_container .show,#warnings_container .focus').forEach( el => el.classList.remove('show','focus') ); removeAttr('#warnings_container',['class','data-funcname']);
  4129. }
  4130. function warningButtons(id) { // ===> WARNING BUTTONs: what to do after warning button click
  4131. let funcName = getData('#warnings_container','funcname') || '', args = getData('#warnings_container','args') || '';
  4132. switch(id) {
  4133. case 'warning_btn_save': // Save/Don't Save Buttons
  4134. switch(true) { // After clicking Save/Don't Save Button...
  4135. case !isTopWindow(): messageSend('top_body',getData('#warnings_container','funcname'),getData('#warnings_container','funcname'),[getData('#warnings_container','args')]); break; // funcName to top
  4136. default: removeClass('body','iframe_edited'); doFunction(funcName,args); focusEl('#top_body'); // remove iframe_edited class; do the function, if any; focus sidebar
  4137. }
  4138. deleteData('#warnings_container','funcname'); deleteData('#warnings_container',args); // remove warnings_container data
  4139. if (id === 'warning_btn_save') { getEl('#save_text_link').click(); } break; // if id = save button, click save text link
  4140. case 'warning_btn_dont_save': if ( !isTopWindow() ) { messageSend('top_body','close_content'); } else { doFunction(funcName,[args,'ok']); } break;
  4141. case 'warning_btn_cancel':
  4142. switch(true) {
  4143. case isTopWindow(): if ( hasClass('body','focus_content') ) { focusEl('#content_pane'); } else { focusEl('#top_body'); } break; // Cancel Button
  4144. case !isTopWindow():
  4145. if ( getData('#warnings_container','args') === 'warning_btn_save' && getData('#warnings_container','funcname') === 'closeContent' ) { messageSend('top_body','close_content'); } break;
  4146. } break;
  4147. case 'warning_btn_clear': texteditorClear(); break; // Clear text editor
  4148. case 'warning_btn_ok': // OK Button
  4149. switch(true) {
  4150. case hasClass('#warnings_container','warning_close_font'): closeWarning(); openFontFile('close');
  4151. if ( funcName !== args && !/close_playlist|close_font/.test(args) )
  4152. { getEl('#'+funcName).click(); } else { getEl('.dirlist_item.selected').click(); } return; // close font file, open file by funcName (if funcName !== args)
  4153. case hasClass('#warnings_container','warning_close_playlist'): window.stop(); closeWarning(); closePlaylist(); closeContent(); showThis(getEl('.dirlist_item.selected').id);
  4154. if ( funcName !== args && !/close_playlist|close_font/.test(args) ) { getEl('#'+funcName).click(); } return; // close playlist, open file by
  4155. case hasClass('#warnings_container','warning_local_playlist'): // no break; local playlist
  4156. case hasClass('#warnings_container','warning_local_file'): // no break; local file
  4157. case hasClass('#warnings_container','warning_non_local_file'): open(args,'_blank'); break; // no break; open non_local file in new window
  4158. }
  4159. doFunction(funcName,args); break; // do the function, if any, after clicking OK button
  4160. }
  4161. closeWarning();
  4162. }
  4163. // END WARNINGS
  4164. //============================//
  4165. // FINIS! † DEO GRATIAS † //
  4166. })();
  4167.