diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..7a8f771e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "_assets/js/vendor/ace"] + path = _assets/js/vendor/ace + url = https://github.com/ajaxorg/ace-builds diff --git a/_assets/css/fonts.css b/_assets/css/fonts.css new file mode 100644 index 00000000..1911d377 --- /dev/null +++ b/_assets/css/fonts.css @@ -0,0 +1,137 @@ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-cyrillic.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-greek-ext.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-greek.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-cyrillic.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-greek-ext.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-greek.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} + +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), local('MaterialIcons-Regular'), url(material/icons.woff2) format('woff2'); +} + +.prompt .file-list ul li:before, +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'liga'; +} diff --git a/_assets/css/material/icons.woff2 b/_assets/css/material/icons.woff2 new file mode 100644 index 00000000..9fa21125 Binary files /dev/null and b/_assets/css/material/icons.woff2 differ diff --git a/_assets/css/normalize.css b/_assets/css/normalize.css new file mode 100644 index 00000000..9b77e0eb --- /dev/null +++ b/_assets/css/normalize.css @@ -0,0 +1,461 @@ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Change the default font family in all browsers (opinionated). + * 2. Correct the line height in all browsers. + * 3. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +/* Document + ========================================================================== */ + +html { + font-family: sans-serif; /* 1 */ + line-height: 1.15; /* 2 */ + -ms-text-size-adjust: 100%; /* 3 */ + -webkit-text-size-adjust: 100%; /* 3 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * Remove the outline on focused links when they are also active or hovered + * in all browsers (opinionated). + */ + +a:active, +a:hover { + outline-width: 0; +} + +/** + * 1. Remove the bottom border in Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Change the border, margin, and padding in all browsers (opinionated). + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/_assets/css/roboto/medium-cyrillic-ext.woff2 b/_assets/css/roboto/medium-cyrillic-ext.woff2 new file mode 100644 index 00000000..f63bc9a1 Binary files /dev/null and b/_assets/css/roboto/medium-cyrillic-ext.woff2 differ diff --git a/_assets/css/roboto/medium-cyrillic.woff2 b/_assets/css/roboto/medium-cyrillic.woff2 new file mode 100644 index 00000000..b3ca824d Binary files /dev/null and b/_assets/css/roboto/medium-cyrillic.woff2 differ diff --git a/_assets/css/roboto/medium-greek-ext.woff2 b/_assets/css/roboto/medium-greek-ext.woff2 new file mode 100644 index 00000000..7e1a8078 Binary files /dev/null and b/_assets/css/roboto/medium-greek-ext.woff2 differ diff --git a/_assets/css/roboto/medium-greek.woff2 b/_assets/css/roboto/medium-greek.woff2 new file mode 100644 index 00000000..314cf3f8 Binary files /dev/null and b/_assets/css/roboto/medium-greek.woff2 differ diff --git a/_assets/css/roboto/medium-latin-ext.woff2 b/_assets/css/roboto/medium-latin-ext.woff2 new file mode 100644 index 00000000..604b8935 Binary files /dev/null and b/_assets/css/roboto/medium-latin-ext.woff2 differ diff --git a/_assets/css/roboto/medium-latin.woff2 b/_assets/css/roboto/medium-latin.woff2 new file mode 100644 index 00000000..5f96609d Binary files /dev/null and b/_assets/css/roboto/medium-latin.woff2 differ diff --git a/_assets/css/roboto/medium-vietnamese.woff2 b/_assets/css/roboto/medium-vietnamese.woff2 new file mode 100644 index 00000000..d92b7125 Binary files /dev/null and b/_assets/css/roboto/medium-vietnamese.woff2 differ diff --git a/_assets/css/roboto/normal-cyrillic-ext.woff2 b/_assets/css/roboto/normal-cyrillic-ext.woff2 new file mode 100644 index 00000000..e4546e49 Binary files /dev/null and b/_assets/css/roboto/normal-cyrillic-ext.woff2 differ diff --git a/_assets/css/roboto/normal-cyrillic.woff2 b/_assets/css/roboto/normal-cyrillic.woff2 new file mode 100644 index 00000000..d08397f7 Binary files /dev/null and b/_assets/css/roboto/normal-cyrillic.woff2 differ diff --git a/_assets/css/roboto/normal-greek-ext.woff2 b/_assets/css/roboto/normal-greek-ext.woff2 new file mode 100644 index 00000000..ed0b13ca Binary files /dev/null and b/_assets/css/roboto/normal-greek-ext.woff2 differ diff --git a/_assets/css/roboto/normal-greek.woff2 b/_assets/css/roboto/normal-greek.woff2 new file mode 100644 index 00000000..f630772d Binary files /dev/null and b/_assets/css/roboto/normal-greek.woff2 differ diff --git a/_assets/css/roboto/normal-latin-ext.woff2 b/_assets/css/roboto/normal-latin-ext.woff2 new file mode 100644 index 00000000..0c7aec28 Binary files /dev/null and b/_assets/css/roboto/normal-latin-ext.woff2 differ diff --git a/_assets/css/roboto/normal-latin.woff2 b/_assets/css/roboto/normal-latin.woff2 new file mode 100644 index 00000000..120796bb Binary files /dev/null and b/_assets/css/roboto/normal-latin.woff2 differ diff --git a/_assets/css/roboto/normal-vietnamese.woff2 b/_assets/css/roboto/normal-vietnamese.woff2 new file mode 100644 index 00000000..7936b665 Binary files /dev/null and b/_assets/css/roboto/normal-vietnamese.woff2 differ diff --git a/_assets/css/styles.css b/_assets/css/styles.css new file mode 100644 index 00000000..eb9d8a5b --- /dev/null +++ b/_assets/css/styles.css @@ -0,0 +1,1207 @@ +body { + font-family: 'Roboto', sans-serif; + padding-top: 7.8em; + background-color: #f8f8f8; +} + +* { + box-sizing: border-box; +} + +*, +*:hover, +*:active, +*:focus { + outline: 0 +} + +a { + text-decoration: none; +} + +img { + max-width: 100%; +} + +audio, +video { + width: 100%; +} + +pre { + padding: 1em; + border: 1px solid #e6e6e6; + border-radius: 0.5em; + background-color: #f5f5f5; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; +} + +button { + border: 0; + padding: .5em 1em; + margin-left: .5em; + border-radius: .1em; + cursor: pointer; + background: #2196f3; + color: #fff; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.05); + transition: .1s ease all; +} + +button:hover { + background-color: #1E88E5; +} + +.mobile-only { + display: none !important; +} + +.container { + width: 95%; + max-width: 960px; + margin: 1em auto 0; +} + +i.spin { + animation: 1s spin linear infinite; +} + +.pdf { + width: 100%; + height: calc(100vh - 13em); +} + + +/* * * * * * * * * * * * * * * * + * EDITOR * + * * * * * * * * * * * * * * * */ + +#editor .source { + display: none; +} + +#editor .content { + background: #fff; + padding: 1em 0; +} + +#editor #ace, +#editor h2, +#editor .frontmatter { + width: 95%; + max-width: 960px; + margin: 1em auto 0; +} + +#editor h2 { + margin: 1.5em auto 1em; + color: rgba(0, 0, 0, 0.3); + font-weight: 500; +} + +#editor .ace_gutter { + background-color: #fff; +} + + +/* * * * * * * * * * * * * * * * + * EDITOR - MARKDOWN * + * * * * * * * * * * * * * * * */ + +.frontmatter { + column-count: 3; + column-gap: 1em; + column-fill: balance; + /* display: flex; */ + /* flex-wrap: wrap; */ + /* justify-content: space-between; */ + /* flex-grow: 1; */ +} + +.frontmatter label { + display: block; + width: calc(100% - 1em); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.frontmatter label, +.frontmatter h3 { + font-weight: 500; + margin: 0 0; + color: rgba(0, 0, 0, 0.6); +} + +.frontmatter input, +.frontmatter textarea { + display: block; + width: 100%; + border: 0; + margin-top: .5em; + padding: 0; + line-height: 1; +} + +.frontmatter .block, +.frontmatter fieldset[data-type="array"], +.button { + position: relative; + background: #fff; + border-radius: .2em; + border: 1px solid rgba(0, 0, 0, 0.075); + padding: .5em; + break-inside: avoid; + margin: 0 0 1em; + width: 100%; + display: inline-block; +} + +.frontmatter fieldset[data-type="object"] { + position: relative; + margin: 0; +} + +.frontmatter .button { + background-color: #2196f3; + color: #fff; + cursor: pointer; + text-align: center; +} + +[data-type="array-item"] { + position: relative; +} + +[data-type="array-item"] .action { + top: 0; + right: 0; +} + +.frontmatter textarea { + resize: none; +} + +[data-type="array-item"] input { + width: calc(100% - 1em); +} + +.block .action, +fieldset .action { + position: absolute; + top: .5em; + right: .5em; +} + +.block>.action, +fieldset>.action { + opacity: 0; +} + +.block:hover>.action, +fieldset:hover>.action { + opacity: 1; +} + +.block .action.add, +fieldset .action.add { + right: 1.5em; +} + +.frontmatter .action i { + padding: 0; + font-size: 1em; +} + +fieldset { + border: 0; + padding: 0; +} + +.frontmatter>fieldset h3, +.frontmatter>.group h3 { + font-size: 1.5em; + margin-bottom: .5em; +} + +fieldset h3, +.group h3 { + font-size: 0.9em; +} + + +/* * * * * * * * * * * * * * * * + * ACTION * + * * * * * * * * * * * * * * * */ + +.action { + display: inline-block; + cursor: pointer; + -webkit-transition: 0.2s ease all; + transition: 0.2s ease all; + border: 0; + margin: 0; + color: #546E7A; + border-radius: 50%; +} + +.action.disabled { + opacity: 0.2; + cursor: not-allowed; +} + +.action i { + padding: 0.4em; + -webkit-transition: 0.2s ease-in-out all; + transition: 0.2s ease-in-out all; + border-radius: 50%; +} + +.action:hover i { + background-color: rgba(0, 0, 0, .1); +} + +.action ul { + position: absolute; + top: 0; + color: #7d7d7d; + list-style: none; + margin: 0; + padding: 0; + flex-direction: column; + display: flex; +} + +.action ul li { + line-height: 1; + padding: .7em; + transition: .1s ease background-color; +} + +.action ul li:hover { + background-color: rgba(0, 0, 0, 0.04); +} + + +/* * * * * * * * * * * * * * * * + * NEW FILE/DIR * + * * * * * * * * * * * * * * * */ + +.floating { + position: fixed; + bottom: 1em; + right: 1em; +} + +.floating .action { + background-color: #2196f3 !important; + color: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); +} + +#newdir { + position: fixed; + bottom: 1.3em; + right: 5em; + transition: .2s ease all; + opacity: 0; + border: 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24); + padding: .5em; + width: 22em; + border-radius: .2em; +} + +#newdir.enabled { + opacity: 1; +} + + +/* * * * * * * * * * * * * * * * + * HEADER * + * * * * * * * * * * * * * * * */ + +header { + z-index: 1000; + background-color: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.075); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + width: 100%; + padding: 0; +} + +header a, +header a:hover { + color: inherit; +} + +header p i { + font-size: 1em !important; + color: rgba(255, 255, 255, .31); +} + +header>div { + display: flex; + width: 100%; + padding: 0.5em 0.5em 0.5em 1em; + align-items: center; +} + +header p { + display: inline-block; + margin: 0; + vertical-align: middle; +} + +header p a, +header p a:hover { + color: inherit; +} + +header .action span { + display: none; +} + +header>div div { + vertical-align: middle; + position: relative; +} + +#logout { + border-radius: 0; + margin-left: auto; + padding: .15em; +} + +#click-overlay { + display: none; + position: fixed; + cursor: pointer; + top: 0; + left: 0; + height: 100%; + width: 100%; +} + +#click-overlay.active { + display: block; +} + + +/* * * * * * * * * * * * * * * * + * TOP BAR * + * * * * * * * * * * * * * * * */ + +#top-bar { + height: 4em; +} + +#top-bar>div:nth-child(1) { + margin-right: 1em; + font-weight: 500; + font-size: 1.5em; + line-height: 2; +} + + +/* * * * * * * * * * * * * * * * + * SEARCH BAR * + * * * * * * * * * * * * * * * */ + +#search { + position: relative; + display: flex; + height: 100%; + padding: 0.75em; + vertical-align: middle; + border-radius: 0.3em; + background-color: #f5f5f5; + transition: .1s ease all; + width: 100%; + max-width: 25em; +} + +#search.active { + background-color: #fff; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); +} + +#search.active i, +#search.active input { + color: #212121; +} + +#search i, +#search input { + vertical-align: middle; +} + +#search i { + margin-right: 0.3em; + user-select: none; +} + +#search input { + width: 100%; + border: 0; + outline: 0; + background-color: transparent; +} + +#search.active div { + visibility: visible; + opacity: 1; + top: 100%; +} + +#search ul { + padding: 0; + margin: 0; + list-style: none; +} + +#search li { + margin-bottom: .5em; +} + +#search>div { + position: absolute; + top: 0; + width: 100%; + left: 0; + z-index: 999999; + background-color: #fff; + text-align: left; + color: #ccc; + box-shadow: 0 2px 3px rgba(0, 0, 0, .06), 0 2px 2px rgba(0, 0, 0, .12); + padding: .5em; + border-bottom-left-radius: .3em; + border-bottom-right-radius: .3em; + transition: .1s ease all; + visibility: hidden; + opacity: 0; + overflow-x: hidden; + overflow-y: auto; + max-height: 50vh; +} + +#search>div div { + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; +} + +#search>div p { + width: 100%; + text-align: center; + display: none; + margin: 0; + max-width: none; +} + +#search.ongoing p { + display: block; +} + +#search.active div i, +#sidebar #search.active div i { + color: #ccc; + text-align: center; + margin: 0 auto; + display: table; +} + +#search::-webkit-input-placeholder { + color: rgba(255, 255, 255, .5); +} + +#search:-moz-placeholder { + opacity: 1; + color: rgba(255, 255, 255, .5); +} + +#search::-moz-placeholder { + opacity: 1; + color: rgba(255, 255, 255, .5); +} + +#search:-ms-input-placeholder { + color: rgba(255, 255, 255, .5); +} + + +/* * * * * * * * * * * * * * * * + * BOTTOM BAR * + * * * * * * * * * * * * * * * */ + +#bottom-bar { + background-color: #fafafa; + border-top: 1px solid rgba(0, 0, 0, 0.075); + border-bottom: 1px solid rgba(0, 0, 0, 0.075); + height: 3.8em; +} + +#bottom-bar>div:first-child>* { + display: inline-block; + vertical-align: middle; +} + +#bottom-bar>div:first-child>i { + margin-right: .3em; +} + +#bottom-bar>*:first-child { + margin-right: auto; + max-width: calc(100% - 25em); + width: 100%; +} + +#bottom-bar p { + text-overflow: ellipsis; + overflow: hidden; + width: calc(100% - 3em); + white-space: nowrap; +} + +#more { + display: none; +} + +#file-only { + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.075); + padding-right: .3em; + margin-right: .3em; + transition: .2s ease opacity, visibility; + visibility: visible; +} + +#file-only.disabled { + opacity: 0; + visibility: hidden; +} + +#download ul.active { + top: 0; + right: 0; +} + +#more ul.active { + right: .5em; + top: 4.5em; +} + + +/* * * * * * * * * * * * * * * * + * DROPDOWN * + * * * * * * * * * * * * * * * */ + +.dropdown { + position: fixed; + top: -100%; + right: -100%; + visibility: hidden; + display: flex; + flex-direction: column; + border-radius: .1em; + border-top-left-radius: 0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + background: #fff; + z-index: 9999999; +} + +.dropdown.active { + visibility: visible; +} + +.dropdown .action { + padding: .7em; +} + +.dropdown i { + padding: 0; + vertical-align: middle; +} + +.dropdown span { + display: inline-block; + margin-left: .5em; + font-size: .9em; +} + + +/* * * * * * * * * * * * * * * * + * BREADCRUMBS * + * * * * * * * * * * * * * * * */ + +#previous { + margin-left: -.5em; +} + +#breadcrumbs { + min-width: 7em; +} + +#breadcrumbs.active { + top: 0; + left: 0; + right: auto; +} + + +/* * * * * * * * * * * * * * * * + * LISTING * + * * * * * * * * * * * * * * * */ + +#listing { + max-width: calc(100% - 1.2em); + width: 100%; +} + +#listing h2 { + margin: 0 0 0 0.5em; + font-size: 1em; + color: rgba(0, 0, 0, 0.2); + font-weight: 500; +} + +#listing .item div:last-of-type * { + text-overflow: ellipsis; + overflow: hidden; +} + +#listing>div { + display: flex; + padding: 0; + flex-wrap: wrap; + justify-content: flex-start; + position: relative; +} + +#listing .item { + background-color: #fff; + position: relative; + display: flex; + flex-wrap: nowrap; + color: #6f6f6f; + transition: .1s ease all; + align-items: center; + cursor: pointer; +} + +#listing .item div:last-of-type { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +#listing .item p { + margin: 0; +} + +#listing .item .size, +#listing .item .modified { + font-size: 0.9em; +} + +#listing .item .name { + font-weight: bold; +} + +#listing .item i { + font-size: 4em; + margin-right: 0.1em; + vertical-align: bottom; +} + +#listing h2.message, +.message { + text-align: center; + font-size: 3em; + margin: 1em auto; + display: block !important; + width: 95%; + color: rgba(0, 0, 0, 0.2); + font-weight: 500; +} + +.message i { + font-size: inherit; + vertical-align: middle; +} + + +/* * * * * * * * * * * * * * * * + * LISTING - MOSAIC * + * * * * * * * * * * * * * * * */ + +#listing.mosaic { + margin-top: 1em; +} + +#listing.mosaic .item { + width: calc(33% - 1em); + margin: .5em; + padding: 0.5em; + border-radius: 0.2em; + box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); +} + +#listing.mosaic .item:hover { + box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important; +} + +#listing.mosaic .header { + display: none; +} + +#listing.mosaic .item div:first-of-type { + width: 5em; +} + +#listing.mosaic .item div:last-of-type { + width: calc(100% - 5vw); +} + + +/* * * * * * * * * * * * * * * * + * LISTING - DETAIL * + * * * * * * * * * * * * * * * */ + +#listing.list { + flex-direction: column; + padding-top: 3.25em; + width: 100%; + max-width: 100%; + margin: 0; +} + +#listing.list .item { + width: 100%; + margin: 0; + border: 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 1em; +} + +#listing.list h2 { + display: none; +} + +#listing .item[aria-selected=true] { + background: #2196f3 !important; + color: #fff !important; +} + +#listing.list .item div:first-of-type { + width: 3em; +} + +#listing.list .item div:first-of-type i { + font-size: 2em; +} + +#listing.list .item div:last-of-type { + width: calc(100% - 3em); + display: flex; + align-items: center; +} + +#listing.list .item .name { + width: 50%; +} + +#listing.list .item .size { + width: 25%; +} + +#listing .item.header { + display: none !important; + background-color: #ccc; +} + +#listing.list .header i { + font-size: 1.5em; + vertical-align: middle; + margin-left: .2em; +} + +#listing.list .item.header { + display: flex !important; + background: #fafafa; + position: fixed; + width: 100%; + top: 7.8em; + left: 0; + z-index: 999; + padding: .85em; +} + +#listing.list .item.header>div:first-child { + width: 0; +} + +#listing.list .item.header .name { + margin-right: 3em; +} + +#listing.list .header { + display: flex; + background: #fafafa; + position: fixed; + width: 100%; + top: 7.8em; + left: 0; + z-index: 999; +} + +#listing.list .header a { + color: inherit; +} + +#listing.list .item.header>div:first-child { + width: 0; +} + +#listing.list .name { + font-weight: normal; +} + +#listing.list .item.header .name { + margin-right: 3em; +} + +#listing.list .header span { + vertical-align: middle; +} + +#listing.list .header i { + opacity: 0; + transition: .1s ease all; +} + +#listing.list .header p:hover i, +#listing.list .header .active i { + opacity: 1; +} + +#listing.list .item.header .active { + font-weight: bold; +} + + +/* * * * * * * * * * * * * * * * + * MULTIPLE SELECTION DIALOG * + * * * * * * * * * * * * * * * */ + +#multiple-selection { + position: fixed; + bottom: -4em; + left: 0; + z-index: 99999999; + width: 100%; + background-color: #2196f3; + height: 4em; + display: flex !important; + padding: 0.5em 0.5em 0.5em 1em; + justify-content: space-between; + align-items: center; + transition: .2s ease all; +} + +#multiple-selection.active { + bottom: 0; +} + +#multiple-selection * { + margin: 0; +} + +#multiple-selection p, +#multiple-selection i { + color: #fff; +} + + +/* * * * * * * * * * * * * * * * + * PROMPT * + * * * * * * * * * * * * * * * */ + +.overlay, +.prompt, +.help { + opacity: 0; + z-index: -1; + transition: .1s ease opacity, z-index; +} + +.overlay.active, +.prompt.active, +.help.active { + z-index: 9999999; + opacity: 1; +} + +.overlay { + background-color: rgba(0, 0, 0, 0.5); + position: fixed; + top: 0; + left: 0; + height: 0; + width: 0; +} + +.overlay.active { + height: 100%; + width: 100%; +} + +.prompt, +.help { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 99999999; + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.075); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + padding: 2em; + max-width: 25em; + width: 90%; + max-height: 95%; +} + +.prompt h3, +.help h3 { + margin: 0; + font-weight: 500; + font-size: 1.5em; +} + +.prompt p, +.help p { + font-size: .9em; + color: rgba(0, 0, 0, 0.8); + margin: .5em 0 1em; +} + +.prompt input { + width: 100%; + border: 1px solid #dadada; + line-height: 1; + padding: .3em; +} + +.prompt code { + word-wrap: break-word; +} + +.prompt div, +.help div { + margin-top: 1em; + display: flex; + justify-content: flex-start; + flex-direction: row-reverse; +} + +.prompt .cancel { + background-color: #ECEFF1; + color: #37474F; +} + +.prompt .cancel:hover { + background-color: #e9eaeb; +} + + +/* * * * * * * * * * * * * * * * + * PROMPT - MOVE * + * * * * * * * * * * * * * * * */ + +.prompt .file-list { + flex-direction: initial; + max-height: 50vh; + overflow: auto; +} + +.prompt .file-list ul { + list-style: none; + margin: 0; + padding: 0; + width: 100%; +} + +.prompt .file-list ul li { + width: 100%; + user-select: none; +} + +.prompt .file-list ul li[aria-selected=true] { + background: #2196f3 !important; + color: #fff !important; + transition: .1s ease all; +} + +.prompt .file-list ul li:hover { + background-color: #e9eaeb; + cursor: pointer; +} + +.prompt .file-list ul li:before { + content: "folder"; + color: #6f6f6f; + vertical-align: middle; + padding: 0 .25em; + line-height: 2em; +} + +.prompt .file-list ul li[aria-selected=true]:before { + color: white; +} + + +/* * * * * * * * * * * * * * * * + * HELP * + * * * * * * * * * * * * * * * */ + +.help { + max-width: 24em; + visibility: hidden; + top: -100%; + left: -100%; +} + +.help.active { + visibility: visible; + top: 50%; + left: 50%; +} + +.help ul { + padding: 0; + margin: 1em 0; + list-style: none; +} + + +/* * * * * * * * * * * * * * * * + * FOOTER * + * * * * * * * * * * * * * * * */ + +footer { + font-size: 0.6em; + margin: 2em 0 2em; + text-align: center; + color: grey; +} + +footer a, +footer a:hover { + color: inherit; +} + + +/* * * * * * * * * * * * * * * * + * MEDIA QUERIES * + * * * * * * * * * * * * * * * */ + +@media screen and (max-width: 850px) { + .frontmatter { + column-count: 2; + } +} + +@media screen and (max-width: 650px) { + body { + transition: .2s ease padding; + } + .mobile-only { + display: inherit !important; + } + #top-bar>div:nth-child(1) { + display: none; + } + #bottom-bar>*:first-child { + max-width: calc(100% - 16em) !important; + } + #main-actions { + position: fixed; + top: -100%; + right: -100%; + visibility: hidden; + display: flex; + flex-direction: column; + border-radius: .1em; + border-top-left-radius: 0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + background: #fff; + z-index: 9999999; + } + #main-actions.active { + right: .5em; + top: 4.5em; + visibility: visible; + } + #main-actions .action { + padding: .7em; + border-radius: 0; + align-items: center; + } + #main-actions .action:hover { + background-color: rgba(0, 0, 0, 0.04); + } + #main-actions i { + padding: 0; + vertical-align: middle; + } + #main-actions .action:hover i { + padding: 0; + background-color: transparent; + } + #main-actions span { + display: inline-block; + margin-left: .5em; + font-size: .9em; + } + #listing.list .item .size, + #listing.list .item .modified { + display: none; + } + #listing.list .item .name { + width: 100%; + } + .frontmatter { + column-count: 1; + } +} + +@media screen and (max-width: 450px) { + #bottom-bar p { + display: none !important; + } +} + + +/* * * * * * * * * * * * * * * * + * ANIMATIONS * + * * * * * * * * * * * * * * * */ + +@keyframes spin { + 100% { + -webkit-transform: rotate(-360deg); + transform: rotate(-360deg); + } +} diff --git a/_assets/js/common.js b/_assets/js/common.js new file mode 100644 index 00000000..76cae2a3 --- /dev/null +++ b/_assets/js/common.js @@ -0,0 +1,685 @@ +'use strict' + +var tempID = '_fm_internal_temporary_id' +var ssl = (window.location.protocol === 'https:') +var templates = {} +var selectedItems = [] +var overlay +var clickOverlay + +// Removes an element, if exists, from an array +Array.prototype.removeElement = function (element) { + var i = this.indexOf(element) + if (i !== -1) { + this.splice(i, 1) + } +} + +// Replaces an element inside an array by another +Array.prototype.replaceElement = function (oldElement, newElement) { + var i = this.indexOf(oldElement) + if (i !== -1) { + this[i] = newElement + } +} + +// Sends a costum event to itself +Document.prototype.sendCostumEvent = function (text) { + this.dispatchEvent(new window.CustomEvent(text)) +} + +// Gets the content of a cookie +Document.prototype.getCookie = function (name) { + var re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') + return document.cookie.replace(re, '$1') +} + +// Remove the last directory of an url +var removeLastDirectoryPartOf = function (url) { + var arr = url.split('/') + if (arr.pop() === '') { + arr.pop() + } + return (arr.join('/')) +} + +function getCSSRule (rules) { + for (let i = 0; i < rules.length; i++) { + rules[i] = rules[i].toLowerCase() + } + + let result = null + let find = Array.prototype.find + + find.call(document.styleSheets, styleSheet => { + result = find.call(styleSheet.cssRules, cssRule => { + let found = false + + if (cssRule instanceof CSSStyleRule) { + for (let i = 0; i < rules.length; i++) { + if (cssRule.selectorText.toLowerCase() === rules[i]) { + found = true + } + } + } + + return found + }) + + return result != null + }) + + return result +} + +/* * * * * * * * * * * * * * * * + * * + * BUTTONS * + * * + * * * * * * * * * * * * * * * */ +var buttons = { + previousState: {} +} + +buttons.setLoading = function (name) { + if (typeof this[name] === 'undefined') return + let i = this[name].querySelector('i') + + this.previousState[name] = i.innerHTML + i.style.opacity = 0 + + setTimeout(function () { + i.classList.add('spin') + i.innerHTML = 'autorenew' + i.style.opacity = 1 + }, 200) +} + +// Changes an element to done animation +buttons.setDone = function (name, success = true) { + let i = this[name].querySelector('i') + + i.style.opacity = 0 + + let thirdStep = () => { + i.innerHTML = this.previousState[name] + i.style.opacity = null + + if (selectedItems.length === 0 && document.getElementById('listing')) { + document.sendCostumEvent('changed-selected') + } + } + + let secondStep = () => { + i.style.opacity = 0 + setTimeout(thirdStep, 200) + } + + let firstStep = () => { + i.classList.remove('spin') + i.innerHTML = success + ? 'done' + : 'close' + i.style.opacity = 1 + setTimeout(secondStep, 1000) + } + + setTimeout(firstStep, 200) + return false +} + +/* * * * * * * * * * * * * * * * + * * + * WEBDAV * + * * + * * * * * * * * * * * * * * * */ +var webdav = {} + +webdav.convertURL = function (url) { + return window.location.origin + url.replace(baseURL + '/', webdavURL + '/') +} + +webdav.move = function (oldLink, newLink) { + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + let destination = newLink.replace(baseURL + '/', webdavURL + '/') + + destination = window.location.origin + destination.substring(prefixURL.length) + + request.open('MOVE', webdav.convertURL(oldLink), true) + request.setRequestHeader('Destination', destination) + request.onload = () => { + if (request.status === 201 || request.status === 204) { + resolve() + } else { + reject(request.statusText) + } + } + request.onerror = () => reject(request.statusText) + request.send() + }) +} + +webdav.put = function (link, body, headers = {}) { + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + request.open('PUT', webdav.convertURL(link), true) + + for (let key in headers) { + request.setRequestHeader(key, headers[key]) + } + + request.onload = () => { + if (request.status == 201) { + resolve() + } else { + reject(request.statusText) + } + } + request.onerror = () => reject(request.statusText) + request.send(body) + }) +} + +webdav.propfind = function (link, body, headers = {}) { + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + request.open('PROPFIND', webdav.convertURL(link), true) + + for (let key in headers) { + request.setRequestHeader(key, headers[key]) + } + + request.onload = () => { + if (request.status < 300) { + resolve(request.responseText) + } else { + reject(request.statusText) + } + } + request.onerror = () => reject(request.statusText) + request.send(body) + }) +} + +webdav.delete = function (link) { + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + request.open('DELETE', webdav.convertURL(link), true) + request.onload = () => { + if (request.status === 204) { + resolve() + } else { + reject(request.statusText) + } + } + request.onerror = () => reject(request.statusText) + request.send() + }) +} + +webdav.new = function (link) { + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + request.open((link.endsWith('/') ? 'MKCOL' : 'PUT'), webdav.convertURL(link), true) + request.onload = () => { + if (request.status === 201) { + resolve() + } else { + reject(request.statusText) + } + } + request.onerror = () => reject(request.statusText) + request.send() + }) +} + +/* * * * * * * * * * * * * * * * + * * + * EVENTS * + * * + * * * * * * * * * * * * * * * */ +function closePrompt (event) { + let prompt = document.querySelector('.prompt') + + if (!prompt) return + + if (typeof event !== 'undefined') { + event.preventDefault() + } + + document.querySelector('.overlay').classList.remove('active') + prompt.classList.remove('active') + + setTimeout(() => { + prompt.remove() + }, 100) +} + +function notImplemented (event) { + event.preventDefault() + clickOverlay.click() + + let clone = document.importNode(templates.message.content, true) + clone.querySelector('h3').innerHTML = 'Not implemented' + clone.querySelector('p').innerHTML = "Sorry, but this feature wasn't implemented yet." + + document.querySelector('body').appendChild(clone) + document.querySelector('.overlay').classList.add('active') + document.querySelector('.prompt').classList.add('active') +} + +// Prevent Default event +var preventDefault = function (event) { + event.preventDefault() +} + +function logoutEvent (event) { + let request = new window.XMLHttpRequest() + request.open('GET', window.location.pathname, true, 'username', 'password') + request.send() + request.onreadystatechange = function () { + if (request.readyState === 4) { + window.location = '/' + } + } +} + +function openEvent (event) { + if (event.currentTarget.classList.contains('disabled')) { + return false + } + + let link = '?raw=true' + + if (selectedItems.length) { + link = document.getElementById(selectedItems[0]).dataset.url + link + } else { + link = window.location.pathname + link + } + + window.open(link) + return false +} + +function getHash (event, hash) { + event.preventDefault() + + let request = new window.XMLHttpRequest() + let link + + if (selectedItems.length) { + link = document.getElementById(selectedItems[0]).dataset.url + } else { + link = window.location.pathname + } + + request.open('GET', `${link}?checksum=${hash}`, true) + + request.onload = () => { + if (request.status >= 300) { + console.log(request.statusText) + return + } + event.target.parentElement.innerHTML = request.responseText + } + request.onerror = (e) => console.log(e) + request.send() +} + +function infoEvent (event) { + event.preventDefault() + if (event.currentTarget.classList.contains('disabled')) { + return + } + + let dir = false + let link + + if (selectedItems.length) { + link = document.getElementById(selectedItems[0]).dataset.url + dir = document.getElementById(selectedItems[0]).dataset.dir + } else { + if (document.getElementById('listing') !== null) { + dir = true + } + + link = window.location.pathname + } + + buttons.setLoading('info', false) + + webdav.propfind(link) + .then((text) => { + let parser = new window.DOMParser() + let xml = parser.parseFromString(text, 'text/xml') + let clone = document.importNode(templates.info.content, true) + + let value = xml.getElementsByTagName('displayname') + if (value.length > 0) { + clone.getElementById('display_name').innerHTML = value[0].innerHTML + } else { + clone.getElementById('display_name').innerHTML = xml.getElementsByTagName('D:displayname')[0].innerHTML + } + + value = xml.getElementsByTagName('getcontentlength') + if (value.length > 0) { + clone.getElementById('content_length').innerHTML = value[0].innerHTML + } else { + clone.getElementById('content_length').innerHTML = xml.getElementsByTagName('D:getcontentlength')[0].innerHTML + } + + value = xml.getElementsByTagName('getlastmodified') + if (value.length > 0) { + clone.getElementById('last_modified').innerHTML = value[0].innerHTML + } else { + clone.getElementById('last_modified').innerHTML = xml.getElementsByTagName('D:getlastmodified')[0].innerHTML + } + + if (dir === true || dir === 'true') { + clone.querySelector('.file-only').style.display = 'none' + } + + document.querySelector('body').appendChild(clone) + document.querySelector('.overlay').classList.add('active') + document.querySelector('.prompt').classList.add('active') + buttons.setDone('info', true) + }) + .catch(e => { + buttons.setDone('info', false) + console.log(e) + }) +} + +function deleteOnSingleFile () { + closePrompt() + buttons.setLoading('delete') + + webdav.delete(window.location.pathname) + .then(() => { + window.location.pathname = removeLastDirectoryPartOf(window.location.pathname) + }) + .catch(e => { + buttons.setDone('delete', false) + console.log(e) + }) +} + +function deleteOnListing () { + closePrompt() + buttons.setLoading('delete') + + let promises = [] + + for (let id of selectedItems) { + promises.push(webdav.delete(document.getElementById(id).dataset.url)) + } + + Promise.all(promises) + .then(() => { + listing.reload() + buttons.setDone('delete') + }) + .catch(e => { + console.log(e) + buttons.setDone('delete', false) + }) +} + +// Handles the delete button event +function deleteEvent (event) { + let single = false + + if (!selectedItems.length) { + selectedItems = ['placeholder'] + single = true + } + + let clone = document.importNode(templates.question.content, true) + clone.querySelector('h3').innerHTML = 'Delete files' + + if (single) { + clone.querySelector('form').addEventListener('submit', deleteOnSingleFile) + clone.querySelector('p').innerHTML = `Are you sure you want to delete this file/folder?` + } else { + clone.querySelector('form').addEventListener('submit', deleteOnListing) + clone.querySelector('p').innerHTML = `Are you sure you want to delete ${selectedItems.length} file(s)?` + } + + clone.querySelector('input').remove() + clone.querySelector('.ok').innerHTML = 'Delete' + + document.body.appendChild(clone) + document.querySelector('.overlay').classList.add('active') + document.querySelector('.prompt').classList.add('active') + + return false +} + +function resetSearchText () { + let box = document.querySelector('#search > div div') + + if (user.AllowCommands) { + box.innerHTML = `Search or use one of your supported commands: ${user.Commands.join(", ")}.` + } else { + box.innerHTML = 'Type and press enter to search.' + } +} + +function searchEvent (event) { + if (this.value.length === 0) { + resetSearchText() + return + } + + let value = this.value, + search = document.getElementById('search'), + scrollable = document.querySelector('#search > div'), + box = document.querySelector('#search > div div'), + pieces = value.split(' '), + supported = false + + user.Commands.forEach(function (cmd) { + if (cmd == pieces[0]) { + supported = true + } + }) + + if (!supported || !user.AllowCommands) { + box.innerHTML = 'Press enter to search.' + } else { + box.innerHTML = 'Press enter to execute.' + } + + if (event.keyCode === 13) { + box.innerHTML = '' + search.classList.add('ongoing') + + let url = window.location.host + window.location.pathname + + if (document.getElementById('editor')) { + url = removeLastDirectoryPartOf(url) + } + + let protocol = ssl ? 'wss:' : 'ws:' + + if (supported && user.AllowCommands) { + let conn = new window.WebSocket(`${protocol}//${url}?command=true`) + + conn.onopen = function () { + conn.send(value) + } + + conn.onmessage = function (event) { + box.innerHTML = event.data + scrollable.scrollTop = scrollable.scrollHeight + } + + conn.onclose = function (event) { + search.classList.remove('ongoing') + listing.reload() + } + + return + } + + box.innerHTML = '' + + let ul = box.querySelector('ul') + let conn = new window.WebSocket(`${protocol}//${url}?search=true`) + + conn.onopen = function () { + conn.send(value) + } + + conn.onmessage = function (event) { + ul.innerHTML += '
  • ' + event.data + '
  • ' + scrollable.scrollTop = scrollable.scrollHeight + } + + conn.onclose = function (event) { + search.classList.remove('ongoing') + } + } +} + +function setupSearch () { + let search = document.getElementById('search') + let searchInput = search.querySelector('input') + let searchDiv = search.querySelector('div') + let hover = false + let focus = false + + resetSearchText() + + searchInput.addEventListener('focus', event => { + focus = true + search.classList.add('active') + }) + + searchDiv.addEventListener('mouseover', event => { + hover = true + search.classList.add('active') + }) + + searchInput.addEventListener('blur', event => { + focus = false + if (hover) return + search.classList.remove('active') + }) + + search.addEventListener('mouseleave', event => { + hover = false + if (focus) return + search.classList.remove('active') + }) + + search.addEventListener('click', event => { + search.classList.add('active') + search.querySelector('input').focus() + }) + + searchInput.addEventListener('keyup', searchEvent) +} + +function closeHelp (event) { + event.preventDefault() + + document.querySelector('.help').classList.remove('active') + document.querySelector('.overlay').classList.remove('active') +} + +function openHelp (event) { + closePrompt(event) + + document.querySelector('.help').classList.add('active') + document.querySelector('.overlay').classList.add('active') +} + +window.addEventListener('keydown', (event) => { + if (event.keyCode === 27) { + if (document.querySelector('.help.active')) { + closeHelp(event) + } + } + + if (event.keyCode === 46) { + deleteEvent(event) + } + + if (event.keyCode === 112) { + event.preventDefault() + openHelp(event) + } +}) + +/* * * * * * * * * * * * * * * * + * * + * BOOTSTRAP * + * * + * * * * * * * * * * * * * * * */ + +document.addEventListener('DOMContentLoaded', function (event) { + overlay = document.querySelector('.overlay') + clickOverlay = document.querySelector('#click-overlay') + + buttons.logout = document.getElementById('logout') + buttons.open = document.getElementById('open') + buttons.delete = document.getElementById('delete') + buttons.previous = document.getElementById('previous') + buttons.info = document.getElementById('info') + + // Attach event listeners + buttons.logout.addEventListener('click', logoutEvent) + buttons.open.addEventListener('click', openEvent) + buttons.info.addEventListener('click', infoEvent) + + templates.question = document.querySelector('#question-template') + templates.info = document.querySelector('#info-template') + templates.message = document.querySelector('#message-template') + templates.move = document.querySelector('#move-template') + + if (user.AllowEdit) { + buttons.delete.addEventListener('click', deleteEvent) + } + + let dropdownButtons = document.querySelectorAll('.action[data-dropdown]') + Array.from(dropdownButtons).forEach(button => { + button.addEventListener('click', event => { + button.querySelector('ul').classList.toggle('active') + clickOverlay.classList.add('active') + + clickOverlay.addEventListener('click', event => { + button.querySelector('ul').classList.remove('active') + clickOverlay.classList.remove('active') + }) + }) + }) + + overlay.addEventListener('click', event => { + if (document.querySelector('.help.active')) { + closeHelp(event) + return + } + + closePrompt(event) + }) + + let mainActions = document.getElementById('main-actions') + + document.getElementById('more').addEventListener('click', event => { + event.preventDefault() + event.stopPropagation() + + clickOverlay.classList.add('active') + mainActions.classList.add('active') + + clickOverlay.addEventListener('click', event => { + mainActions.classList.remove('active') + clickOverlay.classList.remove('active') + }) + }) + + setupSearch() + return false +}) diff --git a/_assets/js/editor.js b/_assets/js/editor.js new file mode 100644 index 00000000..38ff40c7 --- /dev/null +++ b/_assets/js/editor.js @@ -0,0 +1,278 @@ +'use strict' + +var editor = {} + +editor.textareaAutoGrow = function () { + let autogrow = function () { + console.log(this.style.height) + this.style.height = 'auto' + this.style.height = (this.scrollHeight) + 'px' + } + + let textareas = document.getElementsByTagName('textarea') + + let addAutoGrow = () => { + Array.from(textareas).forEach(textarea => { + autogrow.bind(textarea)() + textarea.addEventListener('keyup', autogrow) + }) + } + + addAutoGrow() + window.addEventListener('resize', addAutoGrow) +} + +editor.toggleSourceEditor = function (event) { + event.preventDefault() + + if (document.querySelector('[data-kind="content-only"]')) { + window.location = window.location.pathname + '?visual=true' + return + } + + window.location = window.location.pathname + '?visual=false' +} + +function deleteFrontMatterItem (event) { + event.preventDefault() + document.getElementById(this.dataset.delete).remove() +} + +function makeFromBaseTemplate (id, type, name, parent) { + let clone = document.importNode(templates.base.content, true) + clone.querySelector('fieldset').id = id + clone.querySelector('fieldset').dataset.type = type + clone.querySelector('h3').innerHTML = name + clone.querySelector('.delete').dataset.delete = id + clone.querySelector('.delete').addEventListener('click', deleteFrontMatterItem) + clone.querySelector('.add').addEventListener('click', addFrontMatterItem) + + if (parent.classList.contains('frontmatter')) { + parent.insertBefore(clone, document.querySelector('div.button.add')) + return + } + + parent.appendChild(clone) +} + +function makeFromArrayItemTemplate (id, number, parent) { + let clone = document.importNode(templates.arrayItem.content, true) + clone.querySelector('[data-type="array-item"]').id = `${id}-${number}` + clone.querySelector('input').name = id + clone.querySelector('input').id = id + clone.querySelector('div.action').dataset.delete = `${id}-${number}` + clone.querySelector('div.action').addEventListener('click', deleteFrontMatterItem) + parent.querySelector('.group').appendChild(clone) + document.getElementById(`${id}-${number}`).querySelector('input').focus() +} + +function makeFromObjectItemTemplate (id, name, parent) { + let clone = document.importNode(templates.objectItem.content, true) + clone.querySelector('.block').id = `block-${id}` + clone.querySelector('.block').dataset.content = id + clone.querySelector('label').for = id + clone.querySelector('label').innerHTML = name + clone.querySelector('input').name = id + clone.querySelector('input').id = id + clone.querySelector('.action').dataset.delete = `block-${id}` + clone.querySelector('.action').addEventListener('click', deleteFrontMatterItem) + + parent.appendChild(clone) + document.getElementById(id).focus() +} + +function addFrontMatterItemPrompt (parent) { + return function (event) { + event.preventDefault() + + let value = event.currentTarget.querySelector('input').value + if (value === '') { + return true + } + + closePrompt(event) + + let name = value.substring(0, value.lastIndexOf(':')), + type = value.substring(value.lastIndexOf(':') + 1, value.length) + + if (type !== '' && type !== 'array' && type !== 'object') { + name = value + } + + name = name.replace(' ', '_') + + let id = name + + if (parent.id != '') { + id = parent.id + '.' + id + } + + if (type == 'array' || type == 'object') { + if (parent.dataset.type == 'parent') { + makeFromBaseTemplate(id, type, name, document.querySelector('.frontmatter')) + return + } + + makeFromBaseTemplate(id, type, name, block) + return + } + + let group = parent.querySelector('.group') + + if (group == null) { + parent.insertAdjacentHTML('afterbegin', '
    ') + group = parent.querySelector('.group') + } + + makeFromObjectItemTemplate(id, name, group) + } +} + +function addFrontMatterItem (event) { + event.preventDefault() + + let parent = event.currentTarget.parentNode, + type = parent.dataset.type + + // If the block is an array + if (type === 'array') { + let id = parent.id + '[]', + count = parent.querySelectorAll('.group > div').length, + fieldsets = parent.getElementsByTagName('fieldset') + + if (fieldsets.length > 0) { + let itemType = fieldsets[0].dataset.type, + itemID = parent.id + '[' + fieldsets.length + ']', + itemName = fieldsets.length + + makeFromBaseTemplate(itemID, itemType, itemName, parent) + } else { + makeFromArrayItemTemplate(id, count, parent) + } + + return + } + + if (type == 'object' || type == 'parent') { + let clone = document.importNode(templates.question.content, true) + clone.querySelector('form').id = tempID + clone.querySelector('h3').innerHTML = 'New field' + clone.querySelector('p').innerHTML = 'Write the field name and then press enter. If you want to create an array or an object, end the name with :array or :object.' + clone.querySelector('.ok').innerHTML = 'Create' + clone.querySelector('form').addEventListener('submit', addFrontMatterItemPrompt(parent)) + clone.querySelector('form').classList.add('active') + document.querySelector('body').appendChild(clone) + + document.querySelector('.overlay').classList.add('active') + document.getElementById(tempID).classList.add('active') + } + + return false +} + +document.addEventListener('DOMContentLoaded', (event) => { + if (!document.getElementById('editor')) return + + editor.textareaAutoGrow() + + templates.arrayItem = document.getElementById('array-item-template') + templates.base = document.getElementById('base-template') + templates.objectItem = document.getElementById('object-item-template') + templates.temporary = document.getElementById('temporary-template') + + buttons.save = document.querySelector('#save') + buttons.editSource = document.querySelector('#edit-source') + + if (buttons.editSource) { + buttons.editSource.addEventListener('click', editor.toggleSourceEditor) + } + + let container = document.getElementById('editor'), + kind = container.dataset.kind, + rune = container.dataset.rune + + if (kind != 'frontmatter-only') { + let editor = document.querySelector('.content #ace'), + mode = editor.dataset.mode, + textarea = document.querySelector('textarea[name="content"]'), + aceEditor = ace.edit('ace'), + options = { + wrap: true, + maxLines: Infinity, + theme: 'ace/theme/github', + showPrintMargin: false, + fontSize: '1em', + minLines: 20 + } + + aceEditor.getSession().setMode('ace/mode/' + mode) + aceEditor.getSession().setValue(textarea.value) + aceEditor.getSession().on('change', function () { + textarea.value = aceEditor.getSession().getValue() + }) + + if (mode == 'markdown') options.showGutter = false + aceEditor.setOptions(options) + } + + let deleteFrontMatterItemButtons = document.getElementsByClassName('delete') + Array.from(deleteFrontMatterItemButtons).forEach(button => { + button.addEventListener('click', deleteFrontMatterItem) + }) + + let addFrontMatterItemButtons = document.getElementsByClassName('add') + Array.from(addFrontMatterItemButtons).forEach(button => { + button.addEventListener('click', addFrontMatterItem) + }) + + let saveContent = function () { + let data = form2js(document.querySelector('form')) + + if (typeof data.content === 'undefined' && kind !== 'frontmatter-only') { + data.content = '' + } + + if (typeof data.content === 'number') { + data.content = data.content.toString() + } + + let request = new XMLHttpRequest() + + buttons.setLoading('save') + + webdav.put(window.location.pathname, JSON.stringify(data), { + 'Kind': kind, + 'Rune': rune + }) + .then(() => { + buttons.setDone('save') + }) + .catch(e => { + console.log(e) + buttons.setDone('save', false) + }) + } + + document.querySelector('#save').addEventListener('click', event => { + event.preventDefault() + saveContent() + }) + + document.querySelector('form').addEventListener('submit', (event) => { + event.preventDefault() + saveContent() + }) + + window.addEventListener('keydown', (event) => { + if (event.ctrlKey || event.metaKey) { + switch (String.fromCharCode(event.which).toLowerCase()) { + case 's': + event.preventDefault() + saveContent() + break + } + } + }) + + return false +}) diff --git a/_assets/js/listing.js b/_assets/js/listing.js new file mode 100644 index 00000000..baef101d --- /dev/null +++ b/_assets/js/listing.js @@ -0,0 +1,580 @@ +'use strict' + +var listing = { + selectMultiple: false +} + +listing.reload = function (callback) { + let request = new XMLHttpRequest() + + request.open('GET', window.location) + request.setRequestHeader('Minimal', 'true') + request.send() + request.onreadystatechange = function () { + if (request.readyState === 4) { + if (request.status === 200) { + document.querySelector('body main').innerHTML = request.responseText + listing.addDoubleTapEvent() + + if (typeof callback === 'function') { + callback() + } + } + } + } +} + +listing.itemDragStart = function (event) { + let el = event.target + + for (let i = 0; i < 5; i++) { + if (!el.classList.contains('item')) { + el = el.parentElement + } + } + + event.dataTransfer.setData('id', el.id) + event.dataTransfer.setData('name', el.querySelector('.name').innerHTML) +} + +listing.itemDragOver = function (event) { + event.preventDefault() + let el = event.target + + for (let i = 0; i < 5; i++) { + if (!el.classList.contains('item')) { + el = el.parentElement + } + } + + el.style.opacity = 1 +} + +listing.itemDrop = function (e) { + e.preventDefault() + + let el = e.target, + id = e.dataTransfer.getData('id'), + name = e.dataTransfer.getData('name') + + if (id == '' || name == '') return + + for (let i = 0; i < 5; i++) { + if (!el.classList.contains('item')) { + el = el.parentElement + } + } + + if (el.id === id) return + + let oldLink = document.getElementById(id).dataset.url, + newLink = el.dataset.url + name + + webdav.move(oldLink, newLink) + .then(() => listing.reload()) + .catch(e => console.log(e)) +} + +listing.documentDrop = function (event) { + event.preventDefault() + let dt = event.dataTransfer, + files = dt.files, + el = event.target, + items = document.getElementsByClassName('item') + + for (let i = 0; i < 5; i++) { + if (el != null && !el.classList.contains('item')) { + el = el.parentElement + } + } + + if (files.length > 0) { + if (el != null && el.classList.contains('item') && el.dataset.dir == 'true') { + listing.handleFiles(files, el.querySelector('.name').innerHTML + '/') + return + } + + listing.handleFiles(files, '') + } else { + Array.from(items).forEach(file => { + file.style.opacity = 1 + }) + } +} + +listing.rename = function (event) { + if (!selectedItems.length || selectedItems.length > 1) { + return false + } + + let item = document.getElementById(selectedItems[0]) + + if (item.classList.contains('disabled')) { + return false + } + + let link = item.dataset.url, + field = item.querySelector('.name'), + name = field.innerHTML + + let submit = (event) => { + event.preventDefault() + + let newName = event.currentTarget.querySelector('input').value, + newLink = removeLastDirectoryPartOf(link) + '/' + newName + + closePrompt(event) + buttons.setLoading('rename') + + webdav.move(link, newLink).then(() => { + listing.reload(() => { + newName = btoa(newName) + selectedItems = [newName] + document.getElementById(newName).setAttribute('aria-selected', true) + listing.handleSelectionChange() + }) + + buttons.setDone('rename') + }).catch(error => { + field.innerHTML = name + buttons.setDone('rename', false) + console.log(error) + }) + + return false + } + + let clone = document.importNode(templates.question.content, true) + clone.querySelector('h3').innerHTML = 'Rename' + clone.querySelector('input').value = name + clone.querySelector('.ok').innerHTML = 'Rename' + clone.querySelector('form').addEventListener('submit', submit) + + document.querySelector('body').appendChild(clone) + document.querySelector('.overlay').classList.add('active') + document.querySelector('.prompt').classList.add('active') + + return false +} + +listing.handleFiles = function (files, base) { + buttons.setLoading('upload') + + let promises = [] + + for (let file of files) { + promises.push(webdav.put(window.location.pathname + base + file.name, file)) + } + + Promise.all(promises) + .then(() => { + listing.reload() + buttons.setDone('upload') + }) + .catch(e => { + console.log(e) + buttons.setDone('upload', false) + }) + + return false +} + +listing.unselectAll = function () { + let items = document.getElementsByClassName('item') + Array.from(items).forEach(link => { + link.setAttribute('aria-selected', false) + }) + + selectedItems = [] + + listing.handleSelectionChange() + return false +} + +listing.handleSelectionChange = function (event) { + listing.redefineDownloadURLs() + + let selectedNumber = selectedItems.length, + fileAction = document.getElementById('file-only') + + if (selectedNumber) { + fileAction.classList.remove('disabled') + + if (selectedNumber > 1) { + buttons.open.classList.add('disabled') + buttons.rename.classList.add('disabled') + buttons.info.classList.add('disabled') + } + + if (selectedNumber == 1) { + if (document.getElementById(selectedItems[0]).dataset.dir == 'true') { + buttons.open.classList.add('disabled') + } else { + buttons.open.classList.remove('disabled') + } + + buttons.info.classList.remove('disabled') + buttons.rename.classList.remove('disabled') + } + + return false + } + + buttons.info.classList.remove('disabled') + fileAction.classList.add('disabled') + return false +} + +listing.redefineDownloadURLs = function () { + let files = '' + + for (let i = 0; i < selectedItems.length; i++) { + let url = document.getElementById(selectedItems[i]).dataset.url + files += url.replace(window.location.pathname, '') + ',' + } + + files = files.substring(0, files.length - 1) + files = encodeURIComponent(files) + + let links = document.querySelectorAll('#download ul a') + Array.from(links).forEach(link => { + link.href = '?download=' + link.dataset.format + '&files=' + files + }) +} + +listing.openItem = function (event) { + window.location = event.currentTarget.dataset.url +} + +listing.selectItem = function (event) { + let el = event.currentTarget + + if (selectedItems.length != 0) event.preventDefault() + if (selectedItems.indexOf(el.id) == -1) { + if (!event.ctrlKey && !listing.selectMultiple) listing.unselectAll() + + el.setAttribute('aria-selected', true) + selectedItems.push(el.id) + } else { + el.setAttribute('aria-selected', false) + selectedItems.removeElement(el.id) + } + + listing.handleSelectionChange() + return false +} + +listing.newFileButton = function (event) { + event.preventDefault() + + let clone = document.importNode(templates.question.content, true) + clone.querySelector('h3').innerHTML = 'New file' + clone.querySelector('p').innerHTML = 'End with a trailing slash to create a dir.' + clone.querySelector('.ok').innerHTML = 'Create' + clone.querySelector('form').addEventListener('submit', listing.newFilePrompt) + + document.querySelector('body').appendChild(clone) + document.querySelector('.overlay').classList.add('active') + document.querySelector('.prompt').classList.add('active') +} + +listing.newFilePrompt = function (event) { + event.preventDefault() + buttons.setLoading('new') + + let name = event.currentTarget.querySelector('input').value + + webdav.new(window.location.pathname + name) + .then(() => { + buttons.setDone('new') + listing.reload() + }) + .catch(e => { + console.log(e) + buttons.setDone('new', false) + }) + + closePrompt(event) + return false +} + +listing.updateColumns = function (event) { + let columns = Math.floor(document.getElementById('listing').offsetWidth / 300), + items = getCSSRule(['#listing.mosaic .item', '.mosaic#listing .item']) + + items.style.width = `calc(${100/columns}% - 1em)` +} + +listing.addDoubleTapEvent = function () { + let items = document.getElementsByClassName('item'), + touches = { + id: '', + count: 0 + } + + Array.from(items).forEach(file => { + file.addEventListener('touchstart', event => { + if (touches.id != file.id) { + touches.id = file.id + touches.count = 1 + + setTimeout(() => { + touches.count = 0 + }, 300) + + return + } + + touches.count++ + + if (touches.count > 1) { + window.location = file.dataset.url + } + }) + }) +} + +// Keydown events +window.addEventListener('keydown', (event) => { + if (event.keyCode == 27) { + listing.unselectAll() + + if (document.querySelectorAll('.prompt').length) { + closePrompt(event) + } + } + + if (event.keyCode == 113) { + listing.rename() + } + + if (event.ctrlKey || event.metaKey) { + switch (String.fromCharCode(event.which).toLowerCase()) { + case 's': + event.preventDefault() + window.location = '?download=true' + } + } +}) + +window.addEventListener('resize', () => { + listing.updateColumns() +}) + +listing.selectMoveFolder = function (event) { + if (event.target.getAttribute('aria-selected') === 'true') { + event.target.setAttribute('aria-selected', false) + return + } else { + if (document.querySelector('.file-list li[aria-selected=true]')) { + document.querySelector('.file-list li[aria-selected=true]').setAttribute('aria-selected', false) + } + event.target.setAttribute('aria-selected', true) + return + } +} + +listing.getJSON = function (link) { + return new Promise((resolve, reject) => { + let request = new XMLHttpRequest() + request.open('GET', link) + request.setRequestHeader('Accept', 'application/json') + request.onload = () => { + if (request.status == 200) { + resolve(request.responseText) + } else { + reject(request.statusText) + } + } + request.onerror = () => reject(request.statusText) + request.send() + }) +} + +listing.moveMakeItem = function (url, name) { + let node = document.createElement('li'), + count = 0 + + node.dataset.url = url + node.innerHTML = name + node.setAttribute('aria-selected', false) + + node.addEventListener('dblclick', listing.moveDialogNext) + node.addEventListener('click', listing.selectMoveFolder) + node.addEventListener('touchstart', event => { + count++ + + setTimeout(() => { + count = 0 + }, 300) + + if (count > 1) { + listing.moveDialogNext(event) + } + }) + + return node +} + +listing.moveDialogNext = function (event) { + let request = new XMLHttpRequest(), + prompt = document.querySelector('form.prompt.active'), + list = prompt.querySelector('div.file-list ul') + + prompt.addEventListener('submit', listing.moveSelected) + + listing.getJSON(event.target.dataset.url) + .then((data) => { + let dirs = 0 + + prompt.querySelector('ul').innerHTML = '' + prompt.querySelector('code').innerHTML = event.target.dataset.url + + if (event.target.dataset.url != baseURL + '/') { + let node = listing.moveMakeItem(removeLastDirectoryPartOf(event.target.dataset.url) + '/', '..') + list.appendChild(node) + } + + if (JSON.parse(data) == null) { + prompt.querySelector('p').innerHTML = `There aren't any folders in this directory.` + return + } + + for (let f of JSON.parse(data)) { + if (f.IsDir === true) { + dirs++ + list.appendChild(listing.moveMakeItem(f.URL, f.Name)) + } + } + + if (dirs === 0) + prompt.querySelector('p').innerHTML = `There aren't any folders in this directory.` + }) + .catch(e => console.log(e)) +} + +listing.moveSelected = function (event) { + event.preventDefault() + + let promises = [] + buttons.setLoading('move') + + for (let file of selectedItems) { + let fileElement = document.getElementById(file), + destFolder = event.target.querySelector('p code').innerHTML + + if (event.currentTarget.querySelector('li[aria-selected=true]') != null) { + destFolder = event.currentTarget.querySelector('li[aria-selected=true]').dataset.url + } + + let destPath = '/' + destFolder + '/' + fileElement.querySelector('.name').innerHTML + destPath = destPath.replace('//', '/') + + promises.push(webdav.move(fileElement.dataset.url, destPath)) + } + + Promise.all(promises) + .then(() => { + closePrompt(event) + buttons.setDone('move') + listing.reload() + }) + .catch(e => { + console.log(e) + }) +} + +listing.moveEvent = function (event) { + if (event.currentTarget.classList.contains('disabled')) + return + + listing.getJSON(window.location.pathname) + .then((data) => { + let prompt = document.importNode(templates.move.content, true), + list = prompt.querySelector('div.file-list ul'), + dirs = 0 + + prompt.querySelector('form').addEventListener('submit', listing.moveSelected) + prompt.querySelector('code').innerHTML = window.location.pathname + + if (window.location.pathname !== baseURL + '/') { + list.appendChild(listing.moveMakeItem(removeLastDirectoryPartOf(window.location.pathname) + '/', '..')) + } + + for (let f of JSON.parse(data)) { + if (f.IsDir === true) { + dirs++ + list.appendChild(listing.moveMakeItem(f.URL, f.Name)) + } + } + + if (dirs === 0) { + prompt.querySelector('p').innerHTML = `There aren't any folders in this directory.` + } + + document.body.appendChild(prompt) + document.querySelector('.overlay').classList.add('active') + document.querySelector('.prompt').classList.add('active') + }) + .catch(e => console.log(e)) +} + +document.addEventListener('DOMContentLoaded', event => { + listing.updateColumns() + listing.addDoubleTapEvent() + + buttons.rename = document.getElementById('rename') + buttons.upload = document.getElementById('upload') + buttons.new = document.getElementById('new') + buttons.download = document.getElementById('download') + buttons.move = document.getElementById('move') + + document.getElementById('multiple-selection-activate').addEventListener('click', event => { + listing.selectMultiple = true + clickOverlay.click() + + document.getElementById('multiple-selection').classList.add('active') + document.querySelector('body').style.paddingBottom = '4em' + }) + + document.getElementById('multiple-selection-cancel').addEventListener('click', event => { + listing.selectMultiple = false + + document.querySelector('body').style.paddingBottom = '0' + document.getElementById('multiple-selection').classList.remove('active') + }) + + if (user.AllowEdit) { + buttons.move.addEventListener('click', listing.moveEvent) + buttons.rename.addEventListener('click', listing.rename) + } + + let items = document.getElementsByClassName('item') + + if (user.AllowNew) { + buttons.upload.addEventListener('click', (event) => { + document.getElementById('upload-input').click() + }) + + buttons.new.addEventListener('click', listing.newFileButton) + + // Drag and Drop + document.addEventListener('dragover', function (event) { + event.preventDefault() + }, false) + + document.addEventListener('dragenter', (event) => { + Array.from(items).forEach(file => { + file.style.opacity = 0.5 + }) + }, false) + + document.addEventListener('dragend', (event) => { + Array.from(items).forEach(file => { + file.style.opacity = 1 + }) + }, false) + + document.addEventListener('drop', listing.documentDrop, false) + } +}) diff --git a/_assets/js/vendor/ace b/_assets/js/vendor/ace new file mode 160000 index 00000000..0b01260b --- /dev/null +++ b/_assets/js/vendor/ace @@ -0,0 +1 @@ +Subproject commit 0b01260b38f4db87b2b862e0c7f12e7e2e2905fd diff --git a/_assets/js/vendor/form2js.js b/_assets/js/vendor/form2js.js new file mode 100644 index 00000000..2614c194 --- /dev/null +++ b/_assets/js/vendor/form2js.js @@ -0,0 +1,356 @@ +/** + * Copyright (c) 2010 Maxim Vasiliev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Maxim Vasiliev + * Date: 09.09.2010 + * Time: 19:02:33 + */ + + +(function (root, factory) +{ + if (typeof exports !== 'undefined' && typeof module !== 'undefined' && module.exports) { + // NodeJS + module.exports = factory(); + } + else if (typeof define === 'function' && define.amd) + { + // AMD. Register as an anonymous module. + define(factory); + } + else + { + // Browser globals + root.form2js = factory(); + } +}(this, function () +{ + "use strict"; + + /** + * Returns form values represented as Javascript object + * "name" attribute defines structure of resulting object + * + * @param rootNode {Element|String} root form element (or it's id) or array of root elements + * @param delimiter {String} structure parts delimiter defaults to '.' + * @param skipEmpty {Boolean} should skip empty text values, defaults to true + * @param nodeCallback {Function} custom function to get node value + * @param useIdIfEmptyName {Boolean} if true value of id attribute of field will be used if name of field is empty + */ + function form2js(rootNode, delimiter, skipEmpty, nodeCallback, useIdIfEmptyName, getDisabled) + { + getDisabled = getDisabled ? true : false; + if (typeof skipEmpty == 'undefined' || skipEmpty == null) skipEmpty = true; + if (typeof delimiter == 'undefined' || delimiter == null) delimiter = '.'; + if (arguments.length < 5) useIdIfEmptyName = false; + + rootNode = typeof rootNode == 'string' ? document.getElementById(rootNode) : rootNode; + + var formValues = [], + currNode, + i = 0; + + /* If rootNode is array - combine values */ + if (rootNode.constructor == Array || (typeof NodeList != "undefined" && rootNode.constructor == NodeList)) + { + while(currNode = rootNode[i++]) + { + formValues = formValues.concat(getFormValues(currNode, nodeCallback, useIdIfEmptyName, getDisabled)); + } + } + else + { + formValues = getFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled); + } + + return processNameValues(formValues, skipEmpty, delimiter); + } + + /** + * Processes collection of { name: 'name', value: 'value' } objects. + * @param nameValues + * @param skipEmpty if true skips elements with value == '' or value == null + * @param delimiter + */ + function processNameValues(nameValues, skipEmpty, delimiter) + { + var result = {}, + arrays = {}, + i, j, k, l, + value, + nameParts, + currResult, + arrNameFull, + arrName, + arrIdx, + namePart, + name, + _nameParts; + + for (i = 0; i < nameValues.length; i++) + { + value = nameValues[i].value; + + if (skipEmpty && (value === '' || value === null)) continue; + + name = nameValues[i].name; + _nameParts = name.split(delimiter); + nameParts = []; + currResult = result; + arrNameFull = ''; + + for(j = 0; j < _nameParts.length; j++) + { + namePart = _nameParts[j].split(']['); + if (namePart.length > 1) + { + for(k = 0; k < namePart.length; k++) + { + if (k == 0) + { + namePart[k] = namePart[k] + ']'; + } + else if (k == namePart.length - 1) + { + namePart[k] = '[' + namePart[k]; + } + else + { + namePart[k] = '[' + namePart[k] + ']'; + } + + arrIdx = namePart[k].match(/([a-z_]+)?\[([a-z_][a-z0-9_]+?)\]/i); + if (arrIdx) + { + for(l = 1; l < arrIdx.length; l++) + { + if (arrIdx[l]) nameParts.push(arrIdx[l]); + } + } + else{ + nameParts.push(namePart[k]); + } + } + } + else + nameParts = nameParts.concat(namePart); + } + + for (j = 0; j < nameParts.length; j++) + { + namePart = nameParts[j]; + + if (namePart.indexOf('[]') > -1 && j == nameParts.length - 1) + { + arrName = namePart.substr(0, namePart.indexOf('[')); + arrNameFull += arrName; + + if (!currResult[arrName]) currResult[arrName] = []; + currResult[arrName].push(value); + } + else if (namePart.indexOf('[') > -1) + { + arrName = namePart.substr(0, namePart.indexOf('[')); + arrIdx = namePart.replace(/(^([a-z_]+)?\[)|(\]$)/gi, ''); + + /* Unique array name */ + arrNameFull += '_' + arrName + '_' + arrIdx; + + /* + * Because arrIdx in field name can be not zero-based and step can be + * other than 1, we can't use them in target array directly. + * Instead we're making a hash where key is arrIdx and value is a reference to + * added array element + */ + + if (!arrays[arrNameFull]) arrays[arrNameFull] = {}; + if (arrName != '' && !currResult[arrName]) currResult[arrName] = []; + + if (j == nameParts.length - 1) + { + if (arrName == '') + { + currResult.push(value); + arrays[arrNameFull][arrIdx] = convertValue(currResult[currResult.length - 1]); + } + else + { + currResult[arrName].push(value); + arrays[arrNameFull][arrIdx] = convertValue(currResult[arrName][currResult[arrName].length - 1]); + } + } + else + { + if (!arrays[arrNameFull][arrIdx]) + { + if ((/^[0-9a-z_]+\[?/i).test(nameParts[j+1])) currResult[arrName].push({}); + else currResult[arrName].push([]); + + arrays[arrNameFull][arrIdx] = convertValue(currResult[arrName][currResult[arrName].length - 1]); + } + } + + currResult = convertValue(arrays[arrNameFull][arrIdx]); + } + else + { + arrNameFull += namePart; + + if (j < nameParts.length - 1) /* Not the last part of name - means object */ + { + if (!currResult[namePart]) currResult[namePart] = {}; + currResult = convertValue(currResult[namePart]); + } + else + { + currResult[namePart] = convertValue(value); + } + } + } + } + + return result; + } + + function convertValue(value) { + if (value == "true") return true; + if (value == "false") return false; + if (!isNaN(value)) return parseInt(value); + return value; + } + + function getFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled) + { + var result = extractNodeValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled); + return result.length > 0 ? result : getSubFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled); + } + + function getSubFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled) + { + var result = [], + currentNode = rootNode.firstChild; + + while (currentNode) + { + result = result.concat(extractNodeValues(currentNode, nodeCallback, useIdIfEmptyName, getDisabled)); + currentNode = currentNode.nextSibling; + } + + return result; + } + + function extractNodeValues(node, nodeCallback, useIdIfEmptyName, getDisabled) { + if (node.disabled && !getDisabled) return []; + + var callbackResult, fieldValue, result, fieldName = getFieldName(node, useIdIfEmptyName); + + callbackResult = nodeCallback && nodeCallback(node); + + if (callbackResult && callbackResult.name) { + result = [callbackResult]; + } + else if (fieldName != '' && node.nodeName.match(/INPUT|TEXTAREA/i)) { + fieldValue = getFieldValue(node, getDisabled); + if (null === fieldValue) { + result = []; + } else { + result = [ { name: fieldName, value: fieldValue} ]; + } + } + else if (fieldName != '' && node.nodeName.match(/SELECT/i)) { + fieldValue = getFieldValue(node, getDisabled); + result = [ { name: fieldName.replace(/\[\]$/, ''), value: fieldValue } ]; + } + else { + result = getSubFormValues(node, nodeCallback, useIdIfEmptyName, getDisabled); + } + + return result; + } + + function getFieldName(node, useIdIfEmptyName) + { + if (node.name && node.name != '') return node.name; + else if (useIdIfEmptyName && node.id && node.id != '') return node.id; + else return ''; + } + + + function getFieldValue(fieldNode, getDisabled) + { + if (fieldNode.disabled && !getDisabled) return null; + + switch (fieldNode.nodeName) { + case 'INPUT': + case 'TEXTAREA': + switch (fieldNode.type.toLowerCase()) { + case 'radio': + if (fieldNode.checked && fieldNode.value === "false") return false; + case 'checkbox': + if (fieldNode.checked && fieldNode.value === "true") return true; + if (!fieldNode.checked && fieldNode.value === "true") return false; + if (fieldNode.checked) return fieldNode.value; + break; + + case 'button': + case 'reset': + case 'submit': + case 'image': + return ''; + break; + + default: + return fieldNode.value; + break; + } + break; + + case 'SELECT': + return getSelectedOptionValue(fieldNode); + break; + + default: + break; + } + + return null; + } + + function getSelectedOptionValue(selectNode) + { + var multiple = selectNode.multiple, + result = [], + options, + i, l; + + if (!multiple) return selectNode.value; + + for (options = selectNode.getElementsByTagName("option"), i = 0, l = options.length; i < l; i++) + { + if (options[i].selected) result.push(options[i].value); + } + + return result; + } + + return form2js; + +})); diff --git a/_assets/templates/base.tmpl b/_assets/templates/base.tmpl new file mode 100644 index 00000000..85b9a3ef --- /dev/null +++ b/_assets/templates/base.tmpl @@ -0,0 +1,289 @@ + + +{{ $absURL := .Config.AbsoluteURL }} + + {{.Name}} + + + + + + {{- if ne .User.StyleSheet "" -}} + + {{- end -}} + + + + {{- if .IsDir }} + + {{- else }} + + + + {{- end }} + + +
    +
    +

    File Manager

    + + +
    + exit_to_app +
    +
    + +
    +
    + {{- if ne .Name "/"}} + + {{- end }} + + {{ if ne .Name "/"}}

    {{ .Name }}

    {{ end }} +
    + +
    + {{- if and (not .IsDir) (.User.AllowEdit) }} + {{- if .Editor}} + + {{- if eq .Data.Mode "markdown" }} +
    + remove_red_eye +
    + {{- end }} + + {{- if eq .Data.Visual true }} +
    + code +
    + {{- end }} + {{- end }} + +
    + save +
    + {{- end }} + + {{- if .IsDir }} +
    + open_in_new + See raw +
    + {{- end }} + + {{- if and (.User.AllowEdit) (.IsDir) }} +
    + forward + Move file +
    + {{- end }} + + {{- if and .IsDir .User.AllowEdit }} +
    + mode_edit +
    + {{- end }} + + {{- if and .User.AllowEdit .IsDir }} +
    + deleteDelete +
    + {{- end }} +
    + +
    + more_vert +
    + +
    + {{- if .IsDir }} +
    + {{- if eq .Display "mosaic" }} + + view_listSwitch view + + {{- else }} + + view_moduleSwitch view + + {{- end }} +
    + +
    + check_circleSelect +
    + {{- end }} + + {{- if and (.User.AllowNew) (.IsDir) }} +
    + file_uploadUpload +
    + {{- end }} + + {{- if not .IsDir }} +
    + open_in_new + See raw +
    + {{- end }} + + {{- if and .User.AllowEdit (not .IsDir) }} +
    + deleteDelete +
    + {{- end }} + +
    + {{- if not .IsDir}}{{ end }} + file_downloadDownload + {{- if not .IsDir}}{{ end }} + + {{- if .IsDir }} + + {{- end }} +
    + +
    + infoInfo +
    +
    +
    + +
    +
    + +
    +

    Multiple selection enabled

    +
    + clear +
    +
    + +
    + {{- template "content" . }} +
    + +
    + + {{- if and (.User.AllowNew) (.IsDir) }} +
    +
    + add +
    +
    + {{- end }} + + + + + + + + + +
    +

    Help

    + + + +

    Not available yet

    + + + +
    + +
    +
    + + + + diff --git a/_assets/templates/editor.tmpl b/_assets/templates/editor.tmpl new file mode 100644 index 00000000..d02238d8 --- /dev/null +++ b/_assets/templates/editor.tmpl @@ -0,0 +1,57 @@ +{{ define "content" }} +{{- with .Data }} +
    + {{- if or (eq .Class "frontmatter-only") (eq .Class "complete") }} + {{- if (eq .Class "complete")}} +

    Metadata

    + {{- end }} +
    + {{- template "blocks" .FrontMatter.Content }} +
    Add field
    +
    + {{- end }} + + {{ if or (eq .Class "content-only") (eq .Class "complete") }} + {{ if (eq .Class "complete")}} +

    Body

    + {{ end }} +
    +
    + +
    + {{ end }} +
    +{{- end }} + + + + + + +{{ end }} diff --git a/_assets/templates/frontmatter.tmpl b/_assets/templates/frontmatter.tmpl new file mode 100644 index 00000000..3389da90 --- /dev/null +++ b/_assets/templates/frontmatter.tmpl @@ -0,0 +1,56 @@ +{{ define "blocks" }} +{{ if .Fields }}
    {{ end }} +{{- range $key, $value := .Fields }} + {{- if eq $value.Parent.Type "array" }} +
    + {{- template "value" $value }} +
    + close +
    +
    + {{- else }} +
    + + {{ template "value" $value }} +
    + close +
    +
    + {{- end }} +{{- end }} +{{- if .Fields }}
    {{ end }} + +{{- range $key, $value := .Arrays }} +{{- template "fielset" $value }} +{{- end }} + +{{- range $key, $value := .Objects }} +{{- template "fielset" $value }} +{{- end }} + +{{ end }} + +{{ define "value" }} +{{- if eq .HTMLType "textarea" }} + +{{- else if eq .HTMLType "datetime" }} + +{{- else }} + +{{- end }} +{{ end }} + +{{ define "fielset" }} +
    + {{- if not (eq .Title "") }} +

    {{ .Name }}

    + {{- end }} +
    + add +
    +
    + close +
    + {{- template "blocks" .Content }} +
    +{{ end }} diff --git a/_assets/templates/listing.tmpl b/_assets/templates/listing.tmpl new file mode 100644 index 00000000..da94b962 --- /dev/null +++ b/_assets/templates/listing.tmpl @@ -0,0 +1,103 @@ +{{ define "content" }} +
    +{{- with .Data -}} +
    +
    +
    +
    +

    Name + {{- if eq .Sort "name" -}} + {{- if eq .Order "asc" -}} + arrow_downward + {{- else -}} + arrow_upward + {{- end -}} + {{- else -}} + arrow_downward + {{- end -}} +

    +

    File Size + {{- if eq .Sort "size" -}} + {{- if eq .Order "asc" -}} + arrow_downward + {{- else -}} + arrow_upward + {{- end -}} + {{- else -}} + arrow_downward + {{- end -}} +

    +

    Last modified

    +
    +
    +
    + + {{ if and (eq .NumDirs 0) (eq .NumFiles 0) }} +

    It feels lonely here :'(

    + {{ end }} + + {{- if not (eq .NumDirs 0)}} +

    Folders

    +
    + {{- range .Items }} + {{- if (.IsDir) }} + {{ template "item" .}} + {{- end }} + {{- end }} +
    + {{- end }} + + {{- if not (eq .NumFiles 0)}} +

    Files

    +
    + {{- range .Items }} + {{- if (not .IsDir) }} + {{ template "item" .}} + {{- end }} + {{- end }} +
    + {{- end }} +
    + + +{{- end -}} +{{- end -}} + +{{ define "item" }} +
    +
    + {{- if .IsDir}} + folder + {{- else}} + {{ if eq .Type "image" }} + insert_photo + {{ else if eq .Type "audio" }} + volume_up + {{ else if eq .Type "video" }} + movie + {{ else }} + insert_drive_file + {{ end }} + {{- end}} +
    +
    +

    {{.Name}}

    + {{- if .IsDir}} +

    + {{- else}} +

    {{.HumanSize}}

    + {{- end}} +

    + +

    +
    +
    +{{ end }} diff --git a/_assets/templates/minimal.tmpl b/_assets/templates/minimal.tmpl new file mode 100644 index 00000000..66e0068d --- /dev/null +++ b/_assets/templates/minimal.tmpl @@ -0,0 +1 @@ +{{ template "content" . }} diff --git a/_assets/templates/single.tmpl b/_assets/templates/single.tmpl new file mode 100644 index 00000000..c85ab5a4 --- /dev/null +++ b/_assets/templates/single.tmpl @@ -0,0 +1,23 @@ +{{ define "content" }} +{{ with .Data}} +
    + {{ if eq .Type "image" }} +
    + {{ else if eq .Type "audio" }} + + {{ else if eq .Type "video" }} + + {{ else if eq .Extension ".pdf" }} + + {{ else if eq .Type "blob" }} +

    Download file_download

    + {{ else}} +
    {{ .StringifyContent }}
    + {{ end }} +
    +{{ end }} +{{ end }} diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go new file mode 100644 index 00000000..bb0f3407 --- /dev/null +++ b/cmd/filemanager/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "net/http" + + "github.com/hacdias/filemanager" +) + + +var m *filemanager.FileManager + +func handler(w http.ResponseWriter, r *http.Request) { + _, err := m.ServeHTTP(w, r) + if err != nil { + log.Print(err) + } +} + +func main() { + m = filemanager.New() + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) +} \ No newline at end of file diff --git a/filemanager.go b/filemanager.go index f65c3362..022bc9ab 100644 --- a/filemanager.go +++ b/filemanager.go @@ -49,10 +49,10 @@ type User struct { // Assets are the static and front-end assets, such as JS, CSS and HTML templates. type Assets struct { - requiredJS rice.Box // JS that is always required to have in order to be usable. - Templates rice.Box - CSS rice.Box - JS rice.Box + requiredJS *rice.Box // JS that is always required to have in order to be usable. + Templates *rice.Box + CSS *rice.Box + JS *rice.Box } // Rule is a dissalow/allow rule. @@ -66,16 +66,65 @@ type Rule struct { // CommandFunc ... type CommandFunc func(r *http.Request, c *FileManager, u *User) error -// AbsoluteURL ... +func New() *FileManager { + m := &FileManager{ + User: &User{ + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + Commands: []string{"git", "svn", "hg"}, + Rules: []*Rule{{ + Regex: true, + Allow: false, + Regexp: regexp.MustCompile("\\/\\..+"), + }}, + }, + Users: map[string]*User{}, + BeforeSave: func(r *http.Request, c *FileManager, u *User) error { return nil }, + AfterSave: func(r *http.Request, c *FileManager, u *User) error { return nil }, + Assets: &Assets{ + Templates: rice.MustFindBox("./_assets/templates"), + CSS: rice.MustFindBox("./_assets/css"), + requiredJS: rice.MustFindBox("./_assets/js"), + }, + } + + m.SetScope(".") + m.SetBaseURL("/") + m.SetWebDavURL("/webdav") + + return m +} + func (m FileManager) AbsoluteURL() string { return m.PrefixURL + m.BaseURL } -// AbsoluteWebdavURL ... func (m FileManager) AbsoluteWebdavURL() string { return m.PrefixURL + m.WebDavURL } +func (m *FileManager) SetBaseURL(url string) { + url = strings.TrimPrefix(url, "/") + url = strings.TrimSuffix(url, "/") + url = "/" + url + m.BaseURL = strings.TrimSuffix(url, "/") +} + +func (m *FileManager) SetWebDavURL(url string) { + m.WebDavURL = m.BaseURL + "/" + strings.TrimPrefix(url, "/") + m.User.Handler = &webdav.Handler{ + Prefix: m.WebDavURL, + FileSystem: m.FileSystem, + LockSystem: webdav.NewMemLS(), + } +} + +func (u *User) SetScope(scope string) { + u.Scope = strings.TrimSuffix(scope, "/") + u.FileSystem = webdav.Dir(u.Scope) +} + // Allowed checks if the user has permission to access a directory/file. func (u User) Allowed(url string) bool { var rule *Rule diff --git a/http.go b/http.go index cfe4b852..faa6c140 100644 --- a/http.go +++ b/http.go @@ -6,10 +6,15 @@ import ( "os" "path/filepath" "strings" - - "github.com/mholt/caddy/caddyhttp/httpserver" ) +func matchURL(first, second string) bool { + first = strings.ToLower(first) + second = strings.ToLower(second) + + return strings.HasPrefix(first, second) +} + // ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var ( @@ -21,7 +26,7 @@ func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er // Checks if the URL matches the Assets URL. Returns the asset if the // method is GET and Status Forbidden otherwise. - if httpserver.Path(r.URL.Path).Matches(c.BaseURL + AssetsURL) { + if matchURL(r.URL.Path, c.BaseURL+AssetsURL) { if r.Method == http.MethodGet { return serveAssets(w, r, c) } @@ -37,7 +42,7 @@ func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er } // Checks if the request URL is for the WebDav server - if httpserver.Path(r.URL.Path).Matches(c.WebDavURL) { + if matchURL(r.URL.Path, c.WebDavURL) { // Checks for user permissions relatively to this PATH if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) { return http.StatusForbidden, nil diff --git a/http_assets.go b/http_assets.go index b15513d6..8cf5e564 100644 --- a/http_assets.go +++ b/http_assets.go @@ -9,7 +9,7 @@ import ( ) // AssetsURL is the url of the assets -const AssetsURL = "/_filemanagerinternal" +const AssetsURL = "/_internal" // Serve provides the needed assets for the front-end func serveAssets(w http.ResponseWriter, r *http.Request, m *FileManager) (int, error) { @@ -27,8 +27,13 @@ func serveAssets(w http.ResponseWriter, r *http.Request, m *FileManager) (int, e filename = strings.Replace(filename, "/js/", "", 1) file, err = m.Assets.requiredJS.Bytes(filename) case strings.HasPrefix(filename, "/vendor"): - filename = strings.Replace(filename, "/vendor/", "", 1) - file, err = m.Assets.JS.Bytes(filename) + if m.Assets.JS != nil { + filename = strings.Replace(filename, "/vendor/", "", 1) + file, err = m.Assets.JS.Bytes(filename) + break + } + + fallthrough default: err = errors.New("not found") } diff --git a/info.go b/info.go index 741f03bd..b828476c 100644 --- a/info.go +++ b/info.go @@ -77,7 +77,7 @@ var textExtensions = [...]string{ // RetrieveFileType obtains the mimetype and a simplified internal Type // using the first 512 bytes from the file. -func (i FileInfo) RetrieveFileType() error { +func (i *FileInfo) RetrieveFileType() error { i.Mimetype = mime.TypeByExtension(i.Extension) if i.Mimetype == "" { @@ -128,7 +128,7 @@ func (i FileInfo) RetrieveFileType() error { } // Reads the file. -func (i FileInfo) Read() error { +func (i *FileInfo) Read() error { if len(i.content) != 0 { return nil }