diff --git a/src/download.html b/src/download.html index 7cabea6..164a3c0 100644 --- a/src/download.html +++ b/src/download.html @@ -51,12 +51,18 @@ +
- Additional packages: 0 + Standard features: ☑️ +
+
+
+
+ Extra features: 0
@@ -64,24 +70,17 @@
-
- Always comes with all standard Caddy modules. -

- Optionally select additional packages to include in your build: ⚠️ Only choose plugins you need and trust + + +
+ ⚠️ Only choose plugins you need and trust
-
- - - - - - - -
PackageVersionModules
+
+
-
+ {{include "/includes/footer.html"}} diff --git a/src/resources/css/docs.css b/src/resources/css/docs.css index cbe7a5f..98895f4 100644 --- a/src/resources/css/docs.css +++ b/src/resources/css/docs.css @@ -720,7 +720,7 @@ td code { #module-list td:first-child { word-wrap: break-word; - max-width: 300px; + max-width: 400px; } #module-list .module-link { diff --git a/src/resources/css/download.css b/src/resources/css/download.css index 0fd5371..44e5b75 100644 --- a/src/resources/css/download.css +++ b/src/resources/css/download.css @@ -28,27 +28,43 @@ body { #download { margin: 0; - font-size: 16px; + padding-left: 30px; + padding-right: 30px; font-weight: bold; } -.packages-explanation { - margin: 20px auto; +#filter { + margin-top: 1em; + width: 100%; + padding: 15px; + font-size: 24px; + text-align: center; + border: 0; + border-bottom: 1px solid #ccc; + outline: none; +} + +#filter.found { + color: green; +} +#filter.not-found { + color: #cc0000; } .warning { - margin-left: 1em; - font-size: 12px; + margin-top: 20px; + display: inline-block; + font-size: 14px; font-weight: bold; - padding: 2px 15px; + padding: 5px 15px; border-radius: 1em; - border: 2px solid rgb(255, 201, 0); - color: rgb(206, 151, 0); + color: rgb(255, 208, 0); + background: #333; } input:disabled, select:disabled, -#optional-packages.disabled .optpkg label { +#optional-packages.disabled { cursor: not-allowed !important; } @@ -129,133 +145,93 @@ select:disabled, - -.table-container { - overflow-x: auto; - box-shadow: 0 2px 4px rgba(0, 0, 0, .1); - border-top-left-radius: 8px; - border-top-right-radius: 8px; -} - #optional-packages { - width: 100%; - border-spacing: 0; - border-collapse: collapse; + margin-top: 2em; } -/* #optional-packages tr { - height: 1px; -} */ - -#optional-packages th, -#optional-packages td { - border-bottom: 1px solid #dfeaf0; -} - -#optional-packages th:first-child { border-top-left-radius: 8px; } -#optional-packages th:last-child { border-top-right-radius: 8px; } - -#optional-packages th { - background: #dfe8ec; - text-align: left; - text-transform: uppercase; - letter-spacing: 2px; - color: #54676f; - font-weight: bold; - font-size: 12px; - margin-bottom: 1em; - border-color: #90a2ac; -} - -#optional-packages th:first-child { - padding-left: 4.25em; -} - -#optional-packages td { - background: #fff; - height: 1px; /* TODO: works on Chrome, but not Firefox */ - /* height: 100%; TODO: works on Firefox, but not Chrome */ - /* TODO: - see https://stackoverflow.com/a/34781198/1048862 - (could also do tr with height: 1px which gets ignored, - then td with height: inherit; but this effectively the - same as just doing a td with height: 1px.) - I don't like how either Firefox or Chrome handle this - styling (setting the height of the parent shouldn't be - required at all, the browser is rendering a height - regardless!) but I think I lean toward Firefox's as - being more correct; the hack in Firefox is setting a - flexible 100% height on the parent, rather than the hack - in Chrome which is setting a height that is too small - and stretched or ignored anyway. - */ -} - -#optional-packages th, -#optional-packages .optpkg td:first-child label, -#optional-packages td:not(:first-child) { - padding: 15px; -} - -#optional-packages .optpkg-name { - font-weight: bold; -} - -#optional-packages .optpkg input[type=checkbox] { - transform: scale(1.5); - cursor: pointer; - margin-right: 2em; -} - -#optional-packages .optpkg label { +.package { display: flex; - align-items: center; - height: 100%; - line-height: 1em; + padding: 2em; + background: rgba(255, 255, 255, .4); + border: 10px solid transparent; + box-shadow: 0 1px 1px rgba(0, 0, 0, .2); cursor: pointer; + transition: all 200ms ease-out; } -#optional-packages:not(.disabled) .optpkg label:hover { - background: linear-gradient(90deg, rgba(242,248,253,1) 75%, rgba(242,248,253,0) 100%); +.package:hover { + transform: scale(1.02); + background: #fff; + box-shadow: 0 2px 20px -2px rgba(0, 0, 0, .05); } -#optional-packages .optpkg.selected td { - background-color: #f2f8fd; +.package.selected { + border-color: rgb(25, 97, 192); + background: #fff; } -#optional-packages .optpkg input[type=text] { - font-size: 12px; - padding: 6px; - outline: none; +.package-icon { + font-size: 48px; + margin-right: 20px; +} + +.package-data { + flex-grow: 1; +} + +.package-meta { + float: right; + font-size: 14px; +} + +.package-downloads { + margin-right: 1em; +} + +.package-version-input { text-align: center; - border: 1px solid #ccc; - border-radius: 4px; + max-width: 75px; + padding: 4px; + margin-left: 5px; + border-radius: 5px; + border: 1px solid #aaa; + outline: none; } -#optional-packages .optpkg input[type=text]::placeholder { - font-style: italic; +.package-link { + font-size: 20px; + font-weight: bold; + color: inherit; + word-break: break-all; } -#optional-packages .optpkg-no-modules { - font-size: 12px; +.package-host { + font-size: 14px; +} + +.package-modules { + margin-top: 1em; +} + +.package-no-modules { font-style: italic; color: #555; + font-size: 14px; } -#optional-packages .optpkg-module { - margin-top: .5em; - margin-bottom: .5em; +.module { + margin: 10px 0; + word-wrap: break-word; } -#optional-packages .optpkg-module .module-name { +.module-link { font-weight: bold; font-family: 'PT Mono', monospace; } -#optional-packages .optpkg-module .module-description { - color: #444; - margin-left: 1em; +.module-desc { font-size: 14px; + margin-left: 1em; } @@ -264,6 +240,9 @@ select:disabled, + + + .swal-custom-content { text-align: left; } diff --git a/src/resources/js/common.js b/src/resources/js/common.js index 81578b8..e414eda 100644 --- a/src/resources/js/common.js +++ b/src/resources/js/common.js @@ -14,20 +14,23 @@ function isStandard(packagePath) { return packagePath.startsWith(caddyImportPath); } -function substrBeforeLastDot(s) { - return s.substr(0, s.lastIndexOf('.')) -} - -function substrAfterLastDot(s) { - return s.substr(s.lastIndexOf('.')) -} - -function truncate(str, len) { +function truncate(str, maxLen) { if (!str) return ""; - var startLen = str.length; - str = str.substring(0, len); - if (startLen > len) { - str += "..."; + str = str.trim(); + let firstPeriod = str.match(/\.(\s|$)/); // first dot not in the middle of a word, or at end of string + let terminate = firstPeriod ? firstPeriod.index+1 : str.length; + str = str.substring(0, terminate); + if (str.length <= maxLen) { + return str; } - return str; + return str+"..."; } + +function moduleDocsPreview(mod, maxLen) { + if (!mod || !mod.docs) return ""; + let short = truncate(mod.docs, maxLen); + if (short.indexOf(mod.name) === 0) { + short = short.substr(mod.name.length).trim(); + } + return short; +} \ No newline at end of file diff --git a/src/resources/js/docs-api.js b/src/resources/js/docs-api.js index 12917cd..9cef567 100644 --- a/src/resources/js/docs-api.js +++ b/src/resources/js/docs-api.js @@ -155,29 +155,23 @@ $(function() { }); -function beginRendering(json) { +function beginRendering(json, moduleID) { pageData = json; - console.log("DATA:", pageData); + console.log("PAGE DATA:", pageData); // show notice if module is non-standard - if (pageData.structure.type_name && !isStandard(pageData.structure.type_name)) { - var projectHref = 'https://'+pageData.structure.type_name; - projectHref = substrBeforeLastDot(projectHref); - $('.nonstandard-project-link').attr('href', projectHref).text(projectHref); + if (pageData.repo && !isStandard(pageData.structure.type_name)) { + $('.nonstandard-project-link').attr('href', pageData.repo).text(pageData.repo); $('.nonstandard-notice').prepend(nonStandardFlag).show(); } - if (pageData.structure.doc) { - // for most types, just render their docs - $('#top-doc').html(markdown(pageData.structure.doc)); - } else if (pageData.structure.elems) { - // for maps or arrays, fall through to the underlying type - $('#top-doc').html(markdown(pageData.structure.elems.doc)); - } + // for most types, just render their docs; but for maps or arrays, fall through to underlying type for docs + let rawDocs = pageData.structure.doc ?? pageData.structure.elems; + + $('#top-doc').html(markdown(replaceGoTypeNameWithCaddyModuleName(rawDocs, moduleID))); $('#top-doc').append(makeSubmoduleList("", pageData.structure)); renderData(pageData.structure, 0, "", $('
')); - console.log("DOCS:", pageDocs); if ($('#field-list-contents').text().trim()) { $('#field-list-header').show(); @@ -359,6 +353,23 @@ function canTraverse() { return pageData.breadcrumb != null; } +function replaceGoTypeNameWithCaddyModuleName(docs, moduleID) { + if (!docs || !moduleID) return docs; + + // fully qualified type name + let fqtn = pageData.structure.type_name; + + // extract just the local type name + let typeName = fqtn.substr(fqtn.lastIndexOf('.')+1) + + // replace the type name with the Caddy module ID if it starts the docs. + if (docs.indexOf(typeName) === 0) { + docs = moduleID + docs.substr(typeName.length); + } + + return docs; +} + function markdown(input) { if (!input) { return ""; diff --git a/src/resources/js/docs.js b/src/resources/js/docs.js index a73e517..de71c2f 100644 --- a/src/resources/js/docs.js +++ b/src/resources/js/docs.js @@ -79,4 +79,4 @@ $(function() { .text(text); }); } -}); \ No newline at end of file +}); diff --git a/src/resources/js/download.js b/src/resources/js/download.js index 9dd90f0..27e7651 100644 --- a/src/resources/js/download.js +++ b/src/resources/js/download.js @@ -1,72 +1,68 @@ // download package list as soon as possible $.get("/api/packages").done(function(json) { + // sort package list by most popular, seems to make sense for convenience var packageList = json.result; - const preselectedPackage = new URL(window.location.href).searchParams.getAll("package") ; + packageList.sort((a, b) => { + return b.downloads > a.downloads ? 1 : -1; + }); + + const preselectedPackage = new URL(window.location.href).searchParams.getAll("package"); // wait until the DOM has finished loading before rendering the results $(function() { - const optpkgTemplate = - ''+ - ' '+ - ' '+ - ' '+ - ''; + const packageTemplate = + '
\n'+ + '
📦
\n'+ + '
\n'+ + '
\n'+ + ' downloads: \n'+ + ' version: \n'+ + '
\n'+ + ' \n'+ + '
\n'+ + '
\n'+ + '
\n' + + const moduleTemplate = + '
\n'+ + ' 🔌\n'+ + ' \n'+ + '
\n'; - const optpkgModuleTemplate = - '
'+ - ' '+ - ' '+ - '
'; for (var i = 0; i < packageList.length; i++) { var pkg = packageList[i]; - if (isStandard(pkg.path)) { - // not necessary to show, since these packages - // come with standard distribution - continue; - } - - var $optpkg = $(optpkgTemplate); - $('.optpkg-name', $optpkg).text(pkg.path); - if (preselectedPackage.includes(pkg.path)) { - $('.optpkg-check', $optpkg).prop("checked", true); - $('.optpkg-check', $optpkg).closest('.optpkg').toggleClass("selected"); - } - $('.optpkg-check', $optpkg).change({pkg: pkg}, (event) => { - const element = $(event.currentTarget); - let newUrl = new URL(window.location.href); - let currentSelected = newUrl.searchParams.getAll("package") ; - newUrl.searchParams.delete("package"); - const pkgPath = event.data.pkg.path; - if (element.is(':checked')) { - if (!currentSelected.includes(pkgPath)) { - currentSelected = [...currentSelected, pkgPath]; - } - } else { - const position = currentSelected.indexOf(pkgPath); - if (position >= 0) { - currentSelected.splice(position, 1); - } - } - currentSelected.forEach( (selected) => newUrl.searchParams.append("package", selected)); - history.replaceState({}, "Download Caddy", newUrl.toString()); - }); + var $pkg = $(packageTemplate); + + let { provider, path } = splitVCSProvider(pkg.path); + if (provider) { + var $pkgHost = $('').text(provider); + $('.package-link', $pkg).html($pkgHost).append('
'); + } + $pkgName = $('').text(path); + + $('.package-link', $pkg).append($pkgName); + $('.package-link', $pkg).prop('href', pkg.repo); + $('.package-downloads', $pkg).text(pkg.downloads); + if (preselectedPackage.includes(pkg.path)) { + $($pkg).addClass("selected"); + } if (pkg.modules && pkg.modules.length > 0) { for (var j = 0; j < pkg.modules.length; j++) { var mod = pkg.modules[j]; - var $mod = $(optpkgModuleTemplate); - $('.module-name', $mod).attr('href', '/docs/modules/'+mod.name).text(mod.name); - $('.module-description', $mod).text(truncate(mod.docs, 120)); - $('.optpkg-modules', $optpkg).append($mod); + var $mod = $(moduleTemplate); + $('.module-link', $mod).attr('href', '/docs/modules/'+mod.name).text(mod.name).attr('title', "View module details"); + $('.module-desc', $mod).text(moduleDocsPreview(mod, 120)); + $('.package-modules', $pkg).append($mod); } } else { - $('.optpkg-modules', $optpkg) - .addClass("optpkg-no-modules") - .text('This package does not add any modules. Either it is another kind of plugin (such as a config adapter) or this listing is in error.'); + $('.package-modules', $pkg) + .addClass("package-no-modules") + .text('This package does not add any modules to the JSON config structure. Either it is another kind of plugin (such as a config adapter) or this listing is in error.'); } - $('#optional-packages').append($optpkg); + $('#optional-packages').append($pkg); } updatePage(); }); @@ -86,19 +82,71 @@ $(function() { downloadButtonHtml = $('#download').html(); - // update the page, including the download link, when form fields change - $('#optional-packages').on('change', 'input[type=checkbox]', function() { - $(this).closest('.optpkg').toggleClass('selected'); - updatePage(); - }); - $('#optional-packages').on('change keyup', 'input[name=version]', function() { - updatePage(); + $('#filter').on('search keyup', function(event) { + var count = 0; + var q = $(this).val().trim().toLowerCase(); + + $('.package').each(function() { + if (!q) { + // filter is cleared; show all + this.style.display = ''; + return; + } + + var corpus = $(this).find('.package-link, .module-link, .module-desc').text().trim().toLowerCase(); + + if (corpus.indexOf(q) === -1) { + this.style.display = 'none'; + return; + } + this.style.display = ''; + count++; + }); + + // update color of search input based on results + if (q) { + if (count > 0) { + $('#filter').addClass('found').removeClass('not-found'); + } else { + $('#filter').addClass('not-found').removeClass('found'); + } + } else { + $('#filter').removeClass('found not-found'); + } }); + $('#platform').change(function() { updatePage(); }); - $('#download').click(function(event) { + $('#optional-packages').on('click', '.package', function() { + $(this).toggleClass('selected'); + updatePage(); + + let newUrl = new URL(window.location.href); + let currentSelected = newUrl.searchParams.getAll("package") ; + newUrl.searchParams.delete("package"); + const pkgPath = $('.package-link', $(this)).text().trim(); + if ($(this).hasClass('selected')) { + if (!currentSelected.includes(pkgPath)) { + currentSelected = [...currentSelected, pkgPath]; + } + } else { + const position = currentSelected.indexOf(pkgPath); + if (position >= 0) { + currentSelected.splice(position, 1); + } + } + currentSelected.forEach( (selected) => newUrl.searchParams.append("package", selected)); + history.replaceState({}, document.title, newUrl.toString()); + }); + + // when a link within a package listing is clicked, only operate the link (don't select the package) + $('#optional-packages').on('click', '.package-link, .module-link, .package-version-input', function(event) { + event.stopPropagation(); + }); + + $('#download').click(function() { if ($(this).hasClass('disabled')) { return false; } @@ -180,7 +228,7 @@ function getDownloadLink() { // get plugins and their versions $('#optional-packages .selected').each(function() { // get package path - var p = $('.optpkg-name', this).text().trim(); + var p = $('.package-link', this).text().trim(); // get package version, if user specified one var ver = $('input[name=version]', this).val().trim(); @@ -216,7 +264,7 @@ function handleBuildError(jqxhr, status, error) { } function updatePage() { - $('#package-count').text($('.optpkg.selected').length); + $('#package-count').text($('.package.selected').length); $('#download').attr('href', getDownloadLink()); } @@ -244,4 +292,17 @@ function enableFields() { window.onbeforeunload = null; } +function splitVCSProvider(pkgPath) { + var providers = ["github.com/", "bitbucket.org/"]; + for (var i = 0; i < providers.length; i++) { + if (pkgPath.toLowerCase().indexOf(providers[i]) == 0) { + return { + provider: providers[i], + path: pkgPath.slice(providers[i].length) + }; + } + } + return {provider: "", path: pkgPath}; +} + var downloadButtonHtml; // to restore button to its original contents diff --git a/src/resources/js/module-docs.js b/src/resources/js/module-docs.js index 7ac483c..12bd5a0 100644 --- a/src/resources/js/module-docs.js +++ b/src/resources/js/module-docs.js @@ -9,7 +9,7 @@ if (moduleID) { $(function() { $('#module-docs-container').show(); $('h1').text("Module "+moduleID); - beginRendering(json.result); + beginRendering(json.result, moduleID); }); }); } else { @@ -23,10 +23,17 @@ if (moduleID) { $table = $('#module-list'); for (modID in moduleList) { var val = moduleList[modID]; + + // refine a short preview of the module's docs + let shortDoc = truncate(val.doc, 200); + if (shortDoc && shortDoc.indexOf(modID) === 0) { + shortDoc = shortDoc.substr(modID.length).trim(); + } + var standard = isStandard(val.type_name); var $tr = $(''); $tr.append(''+modID+''+(standard ? '' : ' '+nonStandardFlag)+''); - $tr.append(''+markdown(truncate(val.doc, 200))+''); + $tr.append($('').text(shortDoc)); $table.append($tr); } });