mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2024-04-21 12:32:08 +00:00
chore (migration): viewerpage editor and audio player
This commit is contained in:
@@ -18,7 +18,7 @@ export default class ComponentMenubar extends window.HTMLElement {
|
||||
${basename(decodeURIComponent(location.pathname + location.hash)) || " "}
|
||||
</div>
|
||||
<div class="action-item no-select">
|
||||
<div is="component-dropdown"></div>
|
||||
<!--<div is="component-dropdown"></div>-->
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
@@ -39,7 +39,9 @@ export default class ComponentMenubar extends window.HTMLElement {
|
||||
}
|
||||
|
||||
render($fragment) {
|
||||
this.querySelector(".action-item").appendChild($fragment);
|
||||
const $item = this.querySelector(".action-item");
|
||||
$item.replaceChildren($fragment);
|
||||
animate($item, { time: 250, keyframes: slideYIn(2) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
class ChromecastManager {
|
||||
init() {
|
||||
// TODO: additional rules for setup
|
||||
let src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
||||
if (document.head.querySelector(`script[src="${src}"]`)) return Promise.resolve();
|
||||
|
||||
return new Promise((done) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.onerror = () => done();
|
||||
window["__onGCastApiAvailable"] = function(isAvailable) {
|
||||
if (isAvailable) cast.framework.CastContext.getInstance().setOptions({
|
||||
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||
});
|
||||
done();
|
||||
};
|
||||
document.head.appendChild(script)
|
||||
});
|
||||
}
|
||||
|
||||
origin() {
|
||||
return location.origin;
|
||||
};
|
||||
|
||||
isAvailable() {
|
||||
if (!window.chrome) return false;
|
||||
else if (!window.chrome.cast) return false;
|
||||
return window.chrome.cast.isAvailable;
|
||||
}
|
||||
|
||||
// createLink(apiPath) {
|
||||
// const target = new URL(this.origin() + apiPath);
|
||||
// const shareID = new window.URL(location.href).searchParams.get("share");
|
||||
// if (shareID) target.searchParams.append("share", shareID);
|
||||
// return target.toString();
|
||||
// }
|
||||
|
||||
createRequest(mediaInfo, authorization) {
|
||||
if (!authorization) Promise.error(new Error("Invalid account"));
|
||||
|
||||
// TODO: it would be much much nicer to set the authorization in an HTTP header
|
||||
// but this would require to create a custom web receiver app, setup accounts on
|
||||
// google, etc,... Until that happens, we're setting the authorization within the
|
||||
// url. Once we have that app, the authorisation will come from a customData field
|
||||
// of a chrome.cast.media.LoadRequest
|
||||
const target = new URL(mediaInfo.contentId);
|
||||
target.searchParams.append("authorization", Session.authorization);
|
||||
mediaInfo.contentId = target.toString();
|
||||
return new chrome.cast.media.LoadRequest(mediaInfo);
|
||||
}
|
||||
|
||||
context() {
|
||||
if (!this.isAvailable()) return
|
||||
return window.cast.framework.CastContext.getInstance();
|
||||
}
|
||||
session() {
|
||||
const context = this.context();
|
||||
if (!context) return;
|
||||
return context.getCurrentSession();
|
||||
}
|
||||
media() {
|
||||
const session = this.session();
|
||||
if (!session) return;
|
||||
return session.getMediaSession();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChromecastManager();
|
||||
@@ -2,6 +2,10 @@ export function basename(str, sep = "/") {
|
||||
return str.substr(str.lastIndexOf(sep) + 1);
|
||||
}
|
||||
|
||||
export function extname(str) {
|
||||
return str.substr(str.lastIndexOf(".") + 1).toLowerCase();
|
||||
}
|
||||
|
||||
export function join(baseURL, segment) {
|
||||
return new URL(segment, baseURL).pathname;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,26 @@ export function getSelection$() {
|
||||
return selection$.asObservable();
|
||||
}
|
||||
|
||||
// export function ls() {
|
||||
// return rxjs.from(new Error("missing cache")).pipe(
|
||||
// rxjs.catchError(() => rxjs.of({ files: null })),
|
||||
// rxjs.mergeMap(({ files: filesInCache }) => ajax({
|
||||
// url: `/api/files/ls?path=${path}`,
|
||||
// responseType: "json"
|
||||
// }).pipe(
|
||||
// rxjs.map(({ responseJSON }) => responseJSON),
|
||||
// rxjs.filter(({ filesInRemote }) => {
|
||||
// if (!Array.isArray(filesInCache)) return true;
|
||||
// if (filesInCache.length != filesInRemote.length) return true;
|
||||
// for (let i=0; i<filesInCache.length; i++) {
|
||||
// if (filesInCache[i].name !== filesInRemote[i].name) return true;
|
||||
// }
|
||||
// return false;
|
||||
// }),
|
||||
// )),
|
||||
// )
|
||||
// }
|
||||
|
||||
export function ls() {
|
||||
return rxjs.pipe(
|
||||
rxjs.mergeMap((path) => ajax({
|
||||
|
||||
@@ -4,21 +4,28 @@ import { qs } from "../../lib/dom.js";
|
||||
import { onDestroy } from "../../lib/skeleton/lifecycle.js";
|
||||
import { loadCSS, loadJS } from "../../helpers/loader.js";
|
||||
import { settings_get, settings_put } from "../../lib/settings.js";
|
||||
import Chromecast from "../../lib/chromecast.js";
|
||||
import assert from "../../lib/assert.js";
|
||||
import { basename, extname } from "../../lib/path.js";
|
||||
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
import { render as renderMenubar } from "../../components/menubar.js";
|
||||
import { menubarDownload, menubarChromecast, buildMenubar } from "./common_menubar.js";
|
||||
|
||||
import { ICON } from "./common_icon.js";
|
||||
import { formatTimecode } from "./common_player.js";
|
||||
import { transition, getDownloadUrl } from "./common.js";
|
||||
|
||||
import { getSession } from "../../model/session.js";
|
||||
import { get as getConfig } from "../../model/config.js";
|
||||
|
||||
import "../../components/menubar.js";
|
||||
|
||||
const STATUS_PLAYING = "PLAYING";
|
||||
const STATUS_PAUSED = "PAUSED";
|
||||
const STATUS_BUFFERING = "BUFFERING";
|
||||
|
||||
export default function(render) {
|
||||
export default function(render, { mime }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_audioplayer">
|
||||
<component-menubar></component-menubar>
|
||||
@@ -268,6 +275,9 @@ export default function(render) {
|
||||
case "KeyL":
|
||||
setSeek(Math.min(wavesurfer.getDuration(), currentTime(wavesurfer) + 10), wavesurfer);
|
||||
break;
|
||||
case "KeyF":
|
||||
chromecastLoader();
|
||||
break;
|
||||
case "KeyJ":
|
||||
setSeek(Math.max(0, currentTime(wavesurfer) - 10), wavesurfer);
|
||||
break;
|
||||
@@ -305,11 +315,70 @@ export default function(render) {
|
||||
}),
|
||||
)),
|
||||
));
|
||||
|
||||
// feature9: setup chromecast
|
||||
effect(ready$.pipe(
|
||||
rxjs.tap(() => renderMenubar(buildMenubar(
|
||||
menubarChromecast(),
|
||||
menubarDownload(),
|
||||
))),
|
||||
));
|
||||
// effect(rxjs.combineLatest(
|
||||
// setup$,
|
||||
// getSession(),
|
||||
// getConfig(),
|
||||
// ).pipe(
|
||||
// rxjs.mergeMap(async ([wavesurfer, user, config]) => {
|
||||
// if (!Chromecast.isAvailable()) return;
|
||||
// const filename = basename(decodeURIComponent(location.pathname));
|
||||
// // const link = Chromecast.createLink(getDownloadUrl());
|
||||
// const media = new chrome.cast.media.MediaInfo(
|
||||
// getDownloadUrl(),
|
||||
// mime,
|
||||
// );
|
||||
// media.metadata = new chrome.cast.media.MusicTrackMediaMetadata()
|
||||
// media.metadata.title = "test";
|
||||
// media.metadata.title = filename.substr(0, filename.lastIndexOf(extname(filename)));
|
||||
// media.metadata.subtitle = config.name;
|
||||
// media.metadata.albumName = config.name;
|
||||
// media.metadata.images = [
|
||||
// new chrome.cast.Image(origin + "/assets/icons/music.png"),
|
||||
// ];
|
||||
// wavesurfer.setMute(true);
|
||||
// wavesurfer.pause();
|
||||
|
||||
// const session = Chromecast.session();
|
||||
// if (!session) return
|
||||
// setVolume(session.getVolume() * 100);
|
||||
|
||||
// const req = await Chromecast.createRequest(media, user.authorization);
|
||||
// return session.loadMedia(req);
|
||||
// // .catch((err) => {
|
||||
// // console.error(err);
|
||||
// // notify.send(t("Cannot establish a connection"), "error");
|
||||
// // setIsChromecast(false);
|
||||
// // setIsLoading(false);
|
||||
// // });
|
||||
// }),
|
||||
// ));
|
||||
}
|
||||
|
||||
export function init() {
|
||||
return Promise.all([
|
||||
setup_chromecast(),
|
||||
loadJS(import.meta.url, "../../lib/vendor/wavesurfer.js"),
|
||||
loadCSS(import.meta.url, "./application_audio.css"),
|
||||
]);
|
||||
}
|
||||
|
||||
function setup_chromecast() {
|
||||
if (!("chrome" in window)) {
|
||||
return Promise.resolve();
|
||||
} else if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// if (!CONFIG.enable_chromecast) {
|
||||
// return Promise.resolve();
|
||||
// } else
|
||||
return Chromecast.init();
|
||||
}
|
||||
|
||||
@@ -1,45 +1,334 @@
|
||||
.component_editor {
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.component_editor > div {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.component_editor > div #editor {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #3b4045;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.CodeMirror-sizer > div {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.CodeMirror-foldmarker {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* HIDE LINE NUMBERS ON MOBILE */
|
||||
@media screen and (max-width: 400px) {
|
||||
.CodeMirror-sizer {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
display: none;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* SEARCH */
|
||||
.CodeMirror-dialog {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #525659;
|
||||
z-index: 15;
|
||||
padding: 5px .8em;
|
||||
overflow: hidden;
|
||||
color: #e2e2e2;
|
||||
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-top {
|
||||
border-bottom: 1px solid #eee;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-bottom {
|
||||
border-top: 1px solid #eee;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
width: 20em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog button {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
/* Font stuff */
|
||||
.CodeMirror {
|
||||
font-size: 16px;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
}
|
||||
.CodeMirror-scroll { -webkit-overflow-scrolling: touch; }
|
||||
|
||||
.cm-s-default .cm-header {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Source Code Pro"), local("SourceCodePro-Regular"), url(/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
.cm-s-default .cm-header.cm-level1 {
|
||||
font-size: 19px;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Source Code Pro"), local("SourceCodePro-Regular"), url(/assets/fonts/SourceCodePro-Regular-400-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.CodeMirror {
|
||||
font-size: 14px;
|
||||
}
|
||||
.cm-s-default .cm-header {
|
||||
font-size: 15px;
|
||||
}
|
||||
.cm-s-default .cm-header.cm-level1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
font-style: normal;
|
||||
|
||||
/* Make things more confy */
|
||||
.CodeMirror .CodeMirror-code {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.CodeMirror .CodeMirror-code > div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.CodeMirror[mode="orgmode"] .CodeMirror-code {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.CodeMirror .CodeMirror-line {
|
||||
padding-left: 10%;
|
||||
padding-right: 0;
|
||||
max-width: 950px;
|
||||
}
|
||||
@media screen and (max-width: 1150px) {
|
||||
.CodeMirror .CodeMirror-line {
|
||||
padding-left: 8%;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.CodeMirror .CodeMirror-line {
|
||||
padding-left: 5%;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.CodeMirror .CodeMirror-line {
|
||||
padding-left: 2%;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
.CodeMirror .CodeMirror-linenumber {
|
||||
color: var(--color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.CodeMirror .CodeMirror-gutters {
|
||||
box-shadow: none;
|
||||
background-color: inherit;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Widget stuff */
|
||||
.CodeMirror-linewidget img {
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
height: 300px;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
|
||||
background: var(--dark);
|
||||
}
|
||||
|
||||
/* Code Highlight Theme */
|
||||
.cm-s-default .cm-header {
|
||||
color: #3E7AA6;
|
||||
line-height: 1em;
|
||||
font-weight: 600;
|
||||
src: local("Source Code Pro Semibold"), local("SourceCodePro-Semibold"), url(/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local("Source Code Pro Semibold"), local("SourceCodePro-Semibold"), url(/assets/fonts/SourceCodePro-Semibold-600-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
|
||||
.cm-header.cm-level1 {
|
||||
color: #376e95;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-keyword {
|
||||
color: var(--emphasis-secondary);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header.cm-org-level-star {
|
||||
color: #6f6f6f;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
padding-left: 5px;
|
||||
margin-left: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header.cm-org-todo {
|
||||
color: #FF8355;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header.cm-org-done {
|
||||
color: #3BB27C;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header.cm-org-priority {
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-org-toggle {
|
||||
cursor: pointer;
|
||||
background: var(--light);
|
||||
color: var(--super-light);
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 2px;
|
||||
vertical-align: text-bottom;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-void {
|
||||
display: inline-block;
|
||||
max-width: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header.cm-comment {
|
||||
font-weight: normal;
|
||||
font-size: 0.9em !important;
|
||||
float: right;
|
||||
display: inline-block;
|
||||
color: var(--emphasis);
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
pre.CodeMirror-line {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-link {
|
||||
color: var(--emphasis);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-strong {
|
||||
color: var(--emphasis);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-org-url, .cm-s-default .cm-org-image {
|
||||
color: var(--emphasis) !important;
|
||||
border-bottom: 1px dashed var(--light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable-3 {
|
||||
color: #085;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-comment {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-string, .cm-s-default .cm-string-2 {
|
||||
color: #c41a16;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-def {
|
||||
color: #445588;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-quote {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.CodeMirror-foldmarker {
|
||||
padding-left: 5px;
|
||||
color: var(--color);
|
||||
text-shadow: 1px 1px 10px var(--color);
|
||||
}
|
||||
|
||||
span.CodeMirror-matchingbracket,
|
||||
span.CodeMirror-matchingtag {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* BUGFIX */
|
||||
.CodeMirror-cursor {
|
||||
min-width: 1px !important;
|
||||
}
|
||||
|
||||
/* DAR MODE THEME */
|
||||
.dark-mode .CodeMirror {
|
||||
background: #242424;
|
||||
}
|
||||
.dark-mode .CodeMirror, .dark-mode .CodeMirror .CodeMirror-linenumber, .dark-mode .CodeMirror .cm-variable-2, .dark-mode .CodeMirror .cm-variable-3, .dark-mode .CodeMirror .cm-number, .dark-mode .CodeMirror .cm-quote {
|
||||
color: #f6f3e8;
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-string, .dark-mode .CodeMirror .cm-string-2 {
|
||||
color: #95e454;
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-atom {
|
||||
color: #e5786d;
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-keyword, .dark-mode .CodeMirror .cm-meta, .dark-mode .CodeMirror .cm-header, .dark-mode .CodeMirror .cm-property {
|
||||
color: #8ac6f2;
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-def, .dark-mode .CodeMirror .cm-tag, .dark-mode .CodeMirror .cm-attribute, .dark-mode .CodeMirror .cm-builtin, .dark-mode .CodeMirror .cm-qualifier {
|
||||
color: #cae682;
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-comment {
|
||||
color: #99968b;
|
||||
}
|
||||
.dark-mode .CodeMirror .CodeMirror-cursor {
|
||||
background-color: #99968b;
|
||||
}
|
||||
.dark-mode .CodeMirror .CodeMirror-selected {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-error {
|
||||
color: var(--error);
|
||||
}
|
||||
.dark-mode .CodeMirror .cm-org-url, .dark-mode .CodeMirror .cm-link, .dark-mode .CodeMirror .cm-org-image, .dark-mode .CodeMirror .cm-url {
|
||||
color: #ccaa8f !important;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import { createElement, onDestroy } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { effect } from "../../lib/rx.js";
|
||||
import { animate, slideXIn, opacityOut } from "../../lib/animate.js";
|
||||
import { qs } from "../../lib/dom.js";
|
||||
import { createLoader } from "../../components/loader.js";
|
||||
import { loadCSS, loadJS } from "../../helpers/loader.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
import { extname } from "../../lib/path.js";
|
||||
import { get as getConfig } from "../../model/config.js";
|
||||
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
import ctrlDownloader, { init as initDownloader } from "./application_downloader.js";
|
||||
import { transition, getDownloadUrl } from "./common.js";
|
||||
import { getFile$, saveFile$, transition, getFilename, getCurrentPath } from "./common.js";
|
||||
import { $ICON } from "./common_fab.js";
|
||||
import { fileOptions } from "./model_files.js";
|
||||
|
||||
import "../../components/menubar.js";
|
||||
import "../../components/fab.js";
|
||||
import "../../components/icon.js";
|
||||
|
||||
const TIME_BEFORE_ABORT_EDIT = 5000;
|
||||
|
||||
@@ -18,20 +25,21 @@ export default async function(render) {
|
||||
<div class="component_ide">
|
||||
<component-menubar class="hidden"></component-menubar>
|
||||
<div class="component_editor hidden"></div>
|
||||
<button is="component-fab" class="hidden"></button>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
|
||||
const $editor = qs($page, ".component_editor");
|
||||
const $menubar = qs($page, "component-menubar");
|
||||
const content$ = ajax(getDownloadUrl()).pipe(
|
||||
rxjs.map(({ response }) => response),
|
||||
rxjs.shareReplay(),
|
||||
);
|
||||
const $fab = qs($page, `[is="component-fab"]`);
|
||||
const getConfig$ = getConfig().pipe(rxjs.shareReplay(1));
|
||||
const content$ = new rxjs.ReplaySubject(1);
|
||||
|
||||
// feature1: setup the dom
|
||||
const removeLoader = createLoader($page);
|
||||
effect(rxjs.race(
|
||||
ajax(getDownloadUrl()).pipe(rxjs.map(({ response }) => response)),
|
||||
const setup$ = rxjs.race(
|
||||
getFile$(),
|
||||
ajax("/about").pipe(rxjs.delay(TIME_BEFORE_ABORT_EDIT), rxjs.map(() => null)),
|
||||
).pipe(
|
||||
rxjs.mergeMap((content) => {
|
||||
@@ -46,34 +54,104 @@ export default async function(render) {
|
||||
}
|
||||
return rxjs.of(content);
|
||||
}),
|
||||
rxjs.mergeMap((content) => getConfig$.pipe(
|
||||
rxjs.mergeMap((config) => rxjs.from(loadKeybinding(config.editor)).pipe(rxjs.mapTo(config))),
|
||||
rxjs.map((config) => [content, config]),
|
||||
rxjs.mergeMap((arr) => rxjs.from(loadMode(extname(getFilename()))).pipe(
|
||||
rxjs.map((mode) => arr.concat([mode])),
|
||||
)),
|
||||
rxjs.mergeMap((arr) => fileOptions(getCurrentPath()).pipe(
|
||||
rxjs.map((acl) => arr.concat([acl])),
|
||||
)),
|
||||
)),
|
||||
removeLoader,
|
||||
rxjs.tap((content) => {
|
||||
rxjs.map(([content, config, mode, acl]) => {
|
||||
content$.next(content);
|
||||
$editor.classList.remove("hidden");
|
||||
window.CodeMirror($editor, {
|
||||
const editor = window.CodeMirror($editor, {
|
||||
value: content,
|
||||
lineNumbers: true,
|
||||
// mode: mode,
|
||||
// keyMap: ["emacs", "vim"].indexOf(CONFIG["editor"]) === -1 ?
|
||||
// "sublime" : CONFIG["editor"],
|
||||
mode,
|
||||
keyMap: ["emacs", "vim"].indexOf(config["editor"]) === -1 ? "sublime" : config["editor"],
|
||||
lineWrapping: true,
|
||||
// readOnly: !this.props.readonly,
|
||||
foldOptions: {
|
||||
widget: "...",
|
||||
},
|
||||
readOnly: !/PUT/.test(acl),
|
||||
foldOptions: { widget: "..." },
|
||||
matchBrackets: {},
|
||||
autoCloseBrackets: true,
|
||||
matchTags: { bothTags: true },
|
||||
autoCloseTags: true,
|
||||
});
|
||||
transition($editor);
|
||||
editor.getWrapperElement().setAttribute("mode", mode);
|
||||
if (!("ontouchstart" in window)) editor.focus();
|
||||
if (config["editor"] === "emacs") editor.addKeyMap({
|
||||
"Ctrl-X Ctrl-C": (cm) => window.history.back(),
|
||||
});
|
||||
onDestroy(() => editor.clearHistory());
|
||||
$menubar.classList.remove("hidden");
|
||||
editor.execCommand("save");
|
||||
return editor;
|
||||
}),
|
||||
rxjs.tap((editor) => requestAnimationFrame(() => editor.refresh())),
|
||||
rxjs.share(),
|
||||
rxjs.catchError(ctrlError()),
|
||||
);
|
||||
effect(setup$);
|
||||
|
||||
// feature2: handle resize
|
||||
effect(setup$.pipe(
|
||||
rxjs.mergeMap((editor) => rxjs.fromEvent(window, "resize").pipe(
|
||||
rxjs.tap(() => editor.refresh()),
|
||||
)),
|
||||
));
|
||||
|
||||
// feature3: handle UI for edit
|
||||
effect(setup$.pipe(
|
||||
rxjs.switchMap((editor) => new rxjs.Observable((observer) => editor.on("change", (cm) => observer.next(cm)))),
|
||||
rxjs.mergeMap((editor) => content$.pipe(rxjs.map((oldContent) => [editor, editor.getValue(), oldContent]))),
|
||||
rxjs.tap(async ([editor, newContent = "", oldContent = ""]) => {
|
||||
if ($fab.disabled) return;
|
||||
else if (newContent === oldContent) {
|
||||
await animate($fab, { time: 100, keyframes: opacityOut() });
|
||||
$fab.classList.add("hidden");
|
||||
return
|
||||
}
|
||||
const shouldAnimate = $fab.classList.contains("hidden");
|
||||
$fab.classList.remove("hidden");
|
||||
$fab.render($ICON.SAVING);
|
||||
$fab.onclick = () => CodeMirror.commands.save(editor);
|
||||
// TODO: breadcrumb saving hint *
|
||||
if (shouldAnimate) await animate($fab, { time: 100, keyframes: slideXIn(40) });
|
||||
}),
|
||||
));
|
||||
|
||||
// feature4: save
|
||||
effect(setup$.pipe(
|
||||
rxjs.mergeMap((editor) => new rxjs.Observable((observer) => {
|
||||
CodeMirror.commands.save = (cm) => observer.next(cm);
|
||||
})),
|
||||
rxjs.mergeMap((cm) => {
|
||||
$fab.render($ICON.LOADING)
|
||||
$fab.disabled = true;
|
||||
return rxjs.of(cm.getValue()).pipe(
|
||||
saveFile$(),
|
||||
rxjs.tap((content) => {
|
||||
$fab.removeAttribute("disabled");
|
||||
content$.next(content);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rxjs.catchError(ctrlError()),
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
function has_binary(str) {
|
||||
return /\ufffd/.test(str);
|
||||
let countUnrepresentableChar = 0;
|
||||
for (let i=0; i<str.length; i++) {
|
||||
if (countUnrepresentableChar > 2) return true;
|
||||
else if (str[i] === "\ufffd") countUnrepresentableChar += 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function init() {
|
||||
@@ -81,5 +159,88 @@ export function init() {
|
||||
loadCSS(import.meta.url, "../../lib/vendor/codemirror/lib/codemirror.css"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/lib/codemirror.js"),
|
||||
loadCSS(import.meta.url, "./application_editor.css"),
|
||||
]);
|
||||
]).then(() => Promise.all([
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/keymap/emacs.js"),
|
||||
// search
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/search/searchcursor.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/search/search.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/comment/comment.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/dialog/dialog.js"),
|
||||
// folding
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/foldcode.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/foldgutter.js"),
|
||||
loadCSS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/foldgutter.css"),
|
||||
// editing feature
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/matchbrackets.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/closebrackets.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/closetag.js"),
|
||||
]));
|
||||
}
|
||||
|
||||
function loadMode(ext) {
|
||||
let mode = "text";
|
||||
let before = Promise.resolve();
|
||||
|
||||
if (ext === "org" || ext === "org_archive") {
|
||||
mode = "orgmode";
|
||||
before = loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/xml-fold.js").then(() => loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/matchtags.js"));
|
||||
} else if (ext === "sh") mode = "shell";
|
||||
else if (ext === "py") mode = "python";
|
||||
else if (ext === "html" || ext === "htm") {
|
||||
mode = "htmlmixed";
|
||||
before = Promise.all([
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/xml/xml.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/javascript/javascript.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/css/css.js"),
|
||||
]);
|
||||
} else if (ext === "css") mode = "css";
|
||||
else if (ext === "less" || ext === "scss" || ext === "sass") mode = "sass";
|
||||
else if (ext === "js" || ext === "json") mode = "javascript";
|
||||
else if (ext === "jsx") mode = "jsx";
|
||||
else if (ext === "php" || ext === "php5" || ext === "php4") mode = "php";
|
||||
else if (ext === "elm") mode = "elm";
|
||||
else if (ext === "erl") mode = "erlang";
|
||||
else if (ext === "go") mode = "go";
|
||||
else if (ext === "markdown" || ext === "md") {
|
||||
mode = "yaml-frontmatter";
|
||||
before = Promise.all([
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/markdown/markdown.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/gfm/gfm.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/yaml/yaml.js"),
|
||||
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/mode/overlay.js"),
|
||||
]);
|
||||
} else if (ext === "pl" || ext === "pm") mode = "perl";
|
||||
else if (ext === "clj") mode = "clojure";
|
||||
else if (ext === "el" || ext === "lisp" || ext === "cl" ||
|
||||
ext === "emacs") mode = "commonlisp";
|
||||
else if (ext === "dockerfile") {
|
||||
mode = "dockerfile";
|
||||
before = loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/mode/simple.js");
|
||||
} else if (ext === "R") mode = "r";
|
||||
else if (ext === "makefile") mode = "cmake";
|
||||
else if (ext === "rb") mode = "ruby";
|
||||
else if (ext === "sql") mode = "sql";
|
||||
else if (ext === "xml" || ext === "rss" || ext === "svg" ||
|
||||
ext === "atom") mode = "xml";
|
||||
else if (ext === "yml" || ext === "yaml") mode = "yaml";
|
||||
else if (ext === "lua") mode = "lua";
|
||||
else if (ext === "csv") mode = "spreadsheet";
|
||||
else if (ext === "rs" || ext === "rlib") mode = "rust";
|
||||
else if (ext === "latex" || ext === "tex") mode = "stex";
|
||||
else if (ext === "diff" || ext === "patch") mode = "diff";
|
||||
else if (ext === "sparql") mode = "sparql";
|
||||
else if (ext === "properties") mode = "properties";
|
||||
else if (ext === "c" || ext === "cpp" || ext === "java" ||
|
||||
ext === "h") mode = "clike";
|
||||
|
||||
return before.then(() => loadJS(import.meta.url, `./application_editor/${mode}.js`, { type: "module" }))
|
||||
.catch(() => loadJS(import.meta.url, "./application_editor/text.js", { type: "module" }))
|
||||
.then((module) => Promise.resolve(mode));
|
||||
}
|
||||
|
||||
function loadKeybinding(editor) {
|
||||
if (editor === "emacs" || !editor) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return loadJS(import.meta.url, `./application_editor/keymap_${editor}.js`, { type: "module" });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/clike/clike.js";
|
||||
CodeMirror.__mode = "clike";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/clojure/clojure.js";
|
||||
CodeMirror.__mode = "clojure";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/cmake/cmake.js";
|
||||
CodeMirror.__mode = "cmake";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/commonlisp/commonlisp.js";
|
||||
CodeMirror.__mode = "commonlisp";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/css/css.js";
|
||||
CodeMirror.__mode = "css";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/diff/diff.js";
|
||||
CodeMirror.__mode = "diff";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/dockerfile/dockerfile.js";
|
||||
CodeMirror.__mode = "dockerfile";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/elm/elm.js";
|
||||
CodeMirror.__mode = "elm";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,728 @@
|
||||
export const org_cycle = (cm) => {
|
||||
const pos = cm.getCursor();
|
||||
isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos);
|
||||
};
|
||||
|
||||
|
||||
const state = {
|
||||
stab: "CONTENT",
|
||||
};
|
||||
export const org_set_fold = (cm) => {
|
||||
const cursor = cm.getCursor();
|
||||
set_folding_mode(cm, state.stab);
|
||||
cm.setCursor(cursor);
|
||||
return state.stab;
|
||||
};
|
||||
/*
|
||||
* DONE: Global visibility cycling
|
||||
* TODO: or move to previous table field.
|
||||
*/
|
||||
export const org_shifttab = (cm) => {
|
||||
if (state.stab === "SHOW_ALL") {
|
||||
state.stab = "OVERVIEW";
|
||||
} else if (state.stab === "OVERVIEW") {
|
||||
state.stab = "CONTENT";
|
||||
} else if (state.stab === "CONTENT") {
|
||||
state.stab = "SHOW_ALL";
|
||||
}
|
||||
set_folding_mode(cm, state.stab);
|
||||
return state.stab;
|
||||
};
|
||||
|
||||
|
||||
function set_folding_mode(cm, mode) {
|
||||
if (mode === "OVERVIEW") {
|
||||
folding_mode_overview(cm);
|
||||
} else if (mode === "SHOW_ALL") {
|
||||
folding_mode_all(cm);
|
||||
} else if (mode === "CONTENT") {
|
||||
folding_mode_content(cm);
|
||||
}
|
||||
cm.refresh();
|
||||
|
||||
function folding_mode_overview(cm) {
|
||||
cm.operation(function() {
|
||||
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
|
||||
fold(cm, CodeMirror.Pos(i, 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
function folding_mode_content(cm) {
|
||||
cm.operation(function() {
|
||||
let previous_header = null;
|
||||
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
|
||||
fold(cm, CodeMirror.Pos(i, 0));
|
||||
if (/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true) {
|
||||
const level = cm.getLine(i).replace(/^(\*+).*/, "$1").length;
|
||||
if (previous_header && level > previous_header.level) {
|
||||
unfold(cm, CodeMirror.Pos(previous_header.line, 0));
|
||||
}
|
||||
previous_header = {
|
||||
line: i,
|
||||
level: level,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function folding_mode_all(cm) {
|
||||
cm.operation(function() {
|
||||
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
|
||||
if (/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true) {
|
||||
unfold(cm, CodeMirror.Pos(i, 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Promote heading or move table column to left.
|
||||
*/
|
||||
export const org_metaleft = (cm) => {
|
||||
const line = cm.getCursor().line;
|
||||
_metaleft(cm, line);
|
||||
};
|
||||
function _metaleft(cm, line) {
|
||||
let p = null;
|
||||
if (p = isTitle(cm, line)) {
|
||||
if (p["level"] > 1) {
|
||||
cm.replaceRange(
|
||||
"",
|
||||
{ line: p.start, ch: 0 },
|
||||
{ line: p.start, ch: 1 },
|
||||
);
|
||||
}
|
||||
} else if (p = isItemList(cm, line)) {
|
||||
for (let i=p.start; i<=p.end; i++) {
|
||||
if (p["level"] > 0) {
|
||||
cm.replaceRange(
|
||||
"",
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: 2 },
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (p = isNumberedList(cm, line)) {
|
||||
for (let i=p.start; i<=p.end; i++) {
|
||||
if (p["level"] > 0) {
|
||||
cm.replaceRange(
|
||||
"",
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: 3 },
|
||||
);
|
||||
}
|
||||
}
|
||||
rearrange_list(cm, line);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Demote a subtree, a list item or move table column to right.
|
||||
* In front of a drawer or a block keyword, indent it correctly.
|
||||
*/
|
||||
export const org_metaright = (cm) => {
|
||||
const line = cm.getCursor().line;
|
||||
_metaright(cm, line);
|
||||
};
|
||||
|
||||
function _metaright(cm, line) {
|
||||
let p = null;
|
||||
let tmp = null;
|
||||
if (p = isTitle(cm, line)) {
|
||||
cm.replaceRange("*", { line: p.start, ch: 0 });
|
||||
} else if (p = isItemList(cm, line)) {
|
||||
if (tmp = isItemList(cm, p.start - 1)) {
|
||||
if (p.level < tmp.level + 1) {
|
||||
for (let i=p.start; i<=p.end; i++) {
|
||||
cm.replaceRange(" ", { line: i, ch: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (p = isNumberedList(cm, line)) {
|
||||
if (tmp = isNumberedList(cm, p.start - 1)) {
|
||||
if (p.level < tmp.level + 1) {
|
||||
for (let i=p.start; i<=p.end; i++) {
|
||||
cm.replaceRange(" ", { line: i, ch: 0 });
|
||||
}
|
||||
rearrange_list(cm, p.start);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Insert a new heading or wrap a region in a table
|
||||
*/
|
||||
export const org_meta_return = (cm) => {
|
||||
const line = cm.getCursor().line;
|
||||
const content = cm.getLine(line);
|
||||
let p = null;
|
||||
|
||||
if (p = isItemList(cm, line)) {
|
||||
const level = p.level;
|
||||
cm.replaceRange(
|
||||
"\n"+" ".repeat(level*2)+"- ",
|
||||
{ line: p.end, ch: cm.getLine(p.end).length },
|
||||
);
|
||||
cm.setCursor({ line: p.end+1, ch: level*2+2 });
|
||||
} else if (p = isNumberedList(cm, line)) {
|
||||
const level = p.level;
|
||||
cm.replaceRange(
|
||||
"\n"+" ".repeat(level*3)+(p.n+1)+". ",
|
||||
{ line: p.end, ch: cm.getLine(p.end).length },
|
||||
);
|
||||
cm.setCursor({ line: p.end+1, ch: level*3+3 });
|
||||
rearrange_list(cm, line);
|
||||
} else if (p = isTitle(cm, line)) {
|
||||
const tmp = previousOfType(cm, "title", line);
|
||||
const level = tmp && tmp.level || 1;
|
||||
cm.replaceRange("\n"+"*".repeat(level)+" ", { line: line, ch: content.length });
|
||||
cm.setCursor({ line: line+1, ch: level+1 });
|
||||
} else if (content.trim() === "") {
|
||||
cm.replaceRange("* ", { line: line, ch: 0 });
|
||||
cm.setCursor({ line: line, ch: 2 });
|
||||
} else {
|
||||
cm.replaceRange("\n\n* ", { line: line, ch: content.length });
|
||||
cm.setCursor({ line: line + 2, ch: 2 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const TODO_CYCLES = ["TODO", "DONE", ""];
|
||||
/*
|
||||
* Cycle the thing at point or in the current line, depending on context.
|
||||
* Depending on context, this does one of the following:
|
||||
* - TODO: switch a timestamp at point one day into the past
|
||||
* - DONE: on a headline, switch to the previous TODO keyword.
|
||||
* - TODO: on an item, switch entire list to the previous bullet type
|
||||
* - TODO: on a property line, switch to the previous allowed value
|
||||
* - TODO: on a clocktable definition line, move time block into the past
|
||||
*/
|
||||
export const org_shiftleft = (cm) => {
|
||||
const cycles = [].concat(TODO_CYCLES.slice(0).reverse(), TODO_CYCLES.slice(-1));
|
||||
const line = cm.getCursor().line;
|
||||
const content = cm.getLine(line);
|
||||
const params = isTitle(cm, line);
|
||||
|
||||
if (params === null) return;
|
||||
params["status"] = cycles[cycles.indexOf(params["status"]) + 1];
|
||||
cm.replaceRange(
|
||||
makeTitle(params),
|
||||
{ line: line, ch: 0 },
|
||||
{ line: line, ch: content.length },
|
||||
);
|
||||
};
|
||||
/*
|
||||
* Cycle the thing at point or in the current line, depending on context.
|
||||
* Depending on context, this does one of the following:
|
||||
* - TODO: switch a timestamp at point one day into the future
|
||||
* - DONE: on a headline, switch to the next TODO keyword.
|
||||
* - TODO: on an item, switch entire list to the next bullet type
|
||||
* - TODO: on a property line, switch to the next allowed value
|
||||
* - TODO: on a clocktable definition line, move time block into the future
|
||||
*/
|
||||
export const org_shiftright = (cm) => {
|
||||
cm.operation(() => {
|
||||
const cycles = [].concat(TODO_CYCLES, [TODO_CYCLES[0]]);
|
||||
const line = cm.getCursor().line;
|
||||
const content = cm.getLine(line);
|
||||
const params = isTitle(cm, line);
|
||||
|
||||
if (params === null) return;
|
||||
params["status"] = cycles[cycles.indexOf(params["status"]) + 1];
|
||||
cm.replaceRange(
|
||||
makeTitle(params),
|
||||
{ line: line, ch: 0 },
|
||||
{ line: line, ch: content.length },
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const org_insert_todo_heading = (cm) => {
|
||||
cm.operation(() => {
|
||||
const line = cm.getCursor().line;
|
||||
const content = cm.getLine(line);
|
||||
|
||||
let p = null;
|
||||
if (p = isItemList(cm, line)) {
|
||||
const level = p.level;
|
||||
cm.replaceRange(
|
||||
"\n"+" ".repeat(level*2)+"- [ ] ",
|
||||
{ line: p.end, ch: cm.getLine(p.end).length },
|
||||
);
|
||||
cm.setCursor({ line: line+1, ch: 6+level*2 });
|
||||
} else if (p = isNumberedList(cm, line)) {
|
||||
const level = p.level;
|
||||
cm.replaceRange(
|
||||
"\n"+" ".repeat(level*3)+(p.n+1)+". [ ] ",
|
||||
{ line: p.end, ch: cm.getLine(p.end).length },
|
||||
);
|
||||
cm.setCursor({ line: p.end+1, ch: level*3+7 });
|
||||
rearrange_list(cm, line);
|
||||
} else if (p = isTitle(cm, line)) {
|
||||
const level = p && p.level || 1;
|
||||
cm.replaceRange("\n"+"*".repeat(level)+" TODO ", { line: line, ch: content.length });
|
||||
cm.setCursor({ line: line+1, ch: level+6 });
|
||||
} else if (content.trim() === "") {
|
||||
cm.replaceRange("* TODO ", { line: line, ch: 0 });
|
||||
cm.setCursor({ line: line, ch: 7 });
|
||||
} else {
|
||||
cm.replaceRange("\n\n* TODO ", { line: line, ch: content.length });
|
||||
cm.setCursor({ line: line + 2, ch: 7 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Move subtree up or move table row up.
|
||||
* Calls ‘org-move-subtree-up’ or ‘org-table-move-row’ or
|
||||
* ‘org-move-item-up’, depending on context
|
||||
*/
|
||||
export const org_metaup = (cm) => {
|
||||
cm.operation(() => {
|
||||
const line = cm.getCursor().line;
|
||||
let p = null;
|
||||
|
||||
if (p = isItemList(cm, line)) {
|
||||
const a = isItemList(cm, p.start - 1);
|
||||
if (a) {
|
||||
swap(cm, [p.start, p.end], [a.start, a.end]);
|
||||
rearrange_list(cm, line);
|
||||
}
|
||||
} else if (p = isNumberedList(cm, line)) {
|
||||
const a = isNumberedList(cm, p.start - 1);
|
||||
if (a) {
|
||||
swap(cm, [p.start, p.end], [a.start, a.end]);
|
||||
rearrange_list(cm, line);
|
||||
}
|
||||
} else if (p = isTitle(cm, line)) {
|
||||
let _line = line;
|
||||
let a;
|
||||
do {
|
||||
_line -= 1;
|
||||
if (a = isTitle(cm, _line, p.level)) {
|
||||
break;
|
||||
}
|
||||
} while (_line > 0);
|
||||
|
||||
if (a) {
|
||||
swap(cm, [p.start, p.end], [a.start, a.end]);
|
||||
org_set_fold(cm);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Move subtree down or move table row down.
|
||||
* Calls ‘org-move-subtree-down’ or ‘org-table-move-row’ or
|
||||
* ‘org-move-item-down’, depending on context
|
||||
*/
|
||||
export const org_metadown = (cm) => {
|
||||
cm.operation(() => {
|
||||
const line = cm.getCursor().line;
|
||||
let p = null;
|
||||
|
||||
if (p = isItemList(cm, line)) {
|
||||
const a = isItemList(cm, p.end + 1);
|
||||
if (a) {
|
||||
swap(cm, [p.start, p.end], [a.start, a.end]);
|
||||
}
|
||||
} else if (p = isNumberedList(cm, line)) {
|
||||
const a = isNumberedList(cm, p.end + 1);
|
||||
if (a) {
|
||||
swap(cm, [p.start, p.end], [a.start, a.end]);
|
||||
}
|
||||
rearrange_list(cm, line);
|
||||
} else if (p = isTitle(cm, line)) {
|
||||
const a = isTitle(cm, p.end + 1, p.level);
|
||||
if (a) {
|
||||
swap(cm, [p.start, p.end], [a.start, a.end]);
|
||||
org_set_fold(cm);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const org_shiftmetaright = function(cm) {
|
||||
cm.operation(() => {
|
||||
const line = cm.getCursor().line;
|
||||
let p = null;
|
||||
if (p = isTitle(cm, line)) {
|
||||
_metaright(cm, line);
|
||||
for (let i=p.start + 1; i<=p.end; i++) {
|
||||
if (isTitle(cm, i)) {
|
||||
_metaright(cm, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const org_shiftmetaleft = function(cm) {
|
||||
cm.operation(() => {
|
||||
const line = cm.getCursor().line;
|
||||
let p = null;
|
||||
if (p = isTitle(cm, line)) {
|
||||
if (p.level === 1) return;
|
||||
_metaleft(cm, line);
|
||||
for (let i=p.start + 1; i<=p.end; i++) {
|
||||
if (isTitle(cm, i)) {
|
||||
_metaleft(cm, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function makeTitle(p) {
|
||||
let content = "*".repeat(p["level"])+" ";
|
||||
if (p["status"]) {
|
||||
content += p["status"]+" ";
|
||||
}
|
||||
content += p["content"];
|
||||
return content;
|
||||
}
|
||||
|
||||
function previousOfType(cm, type, line) {
|
||||
let tmp;
|
||||
let i;
|
||||
for (i=line - 1; i>0; i--) {
|
||||
if (type === "list" || type === null) {
|
||||
tmp = isItemList(cm, line);
|
||||
} else if (type === "numbered" || type === null) {
|
||||
tmp = isNumberedList(cm, line);
|
||||
} else if (type === "title" || type === null) {
|
||||
tmp = isTitle(cm, line);
|
||||
}
|
||||
if (tmp !== null) {
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isItemList(cm, line) {
|
||||
const rootLineItem = findRootLine(cm, line);
|
||||
if (rootLineItem === null) return null;
|
||||
line = rootLineItem;
|
||||
const content = cm.getLine(line);
|
||||
|
||||
if (content && (content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ")) return null;
|
||||
const padding = content.replace(/^(\s*).*$/, "$1").length;
|
||||
if (padding % 2 !== 0) return null;
|
||||
return {
|
||||
type: "list",
|
||||
level: padding / 2,
|
||||
content: content.trimLeft().replace(/^\s*\-\s(.*)$/, "$1"),
|
||||
start: line,
|
||||
end: function(_cm, _line) {
|
||||
let line_candidate = _line;
|
||||
let content = null;
|
||||
do {
|
||||
_line += 1;
|
||||
content = _cm.getLine(_line);
|
||||
if (content === undefined || content.trimLeft()[0] === "-") {
|
||||
break;
|
||||
} else if (/^\s+/.test(content)) {
|
||||
line_candidate = _line;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (_line <= _cm.lineCount());
|
||||
return line_candidate;
|
||||
}(cm, line),
|
||||
};
|
||||
|
||||
function findRootLine(_cm, _line) {
|
||||
let content;
|
||||
do {
|
||||
content = _cm.getLine(_line);
|
||||
if (/^\s*\-/.test(content)) return _line;
|
||||
else if (/^\s+/.test(content) === false) {
|
||||
break;
|
||||
}
|
||||
_line -= 1;
|
||||
} while (_line >= 0);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function isNumberedList(cm, line) {
|
||||
const rootLineItem = findRootLine(cm, line);
|
||||
if (rootLineItem === null) return null;
|
||||
line = rootLineItem;
|
||||
const content = cm.getLine(line);
|
||||
|
||||
if (/^[0-9]+[\.\)]\s.*$/.test(content && content.trimLeft()) === false) return null;
|
||||
const padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length;
|
||||
if (padding % 3 !== 0) return null;
|
||||
return {
|
||||
type: "numbered",
|
||||
level: padding / 3,
|
||||
content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, "$1"),
|
||||
start: line,
|
||||
end: function(_cm, _line) {
|
||||
let line_candidate = _line;
|
||||
let content = null;
|
||||
do {
|
||||
_line += 1;
|
||||
content = _cm.getLine(_line);
|
||||
if (content === undefined || /^[0-9]+[\.\)]/.test(content.trimLeft())) {
|
||||
break;
|
||||
} else if (/^\s+/.test(content)) {
|
||||
line_candidate = _line;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (_line <= _cm.lineCount());
|
||||
return line_candidate;
|
||||
}(cm, line),
|
||||
// specific
|
||||
n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")),
|
||||
separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, "$1"),
|
||||
};
|
||||
|
||||
function findRootLine(_cm, _line) {
|
||||
let content;
|
||||
do {
|
||||
content = _cm.getLine(_line);
|
||||
if (/^\s*[0-9]+[\.\)]\s/.test(content)) return _line;
|
||||
else if (/^\s+/.test(content) === false) {
|
||||
break;
|
||||
}
|
||||
_line -= 1;
|
||||
} while (_line >= 0);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function isTitle(cm, line, level) {
|
||||
const content = cm.getLine(line);
|
||||
if (/^\*+\s/.test(content) === false) return null;
|
||||
const match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/);
|
||||
const reference_level = match[1].length;
|
||||
if (level !== undefined && level !== reference_level) return null;
|
||||
if (match === null) return null;
|
||||
return {
|
||||
type: "title",
|
||||
level: reference_level,
|
||||
content: match[3],
|
||||
start: line,
|
||||
end: function(_cm, _line) {
|
||||
let line_candidate = _line;
|
||||
let content = null;
|
||||
do {
|
||||
_line += 1;
|
||||
content = _cm.getLine(_line);
|
||||
if (content === undefined) break;
|
||||
const match = content.match(/^(\*+)\s.*/);
|
||||
if (
|
||||
match && match[1] &&
|
||||
( match[1].length === reference_level || match[1].length < reference_level)
|
||||
) {
|
||||
break;
|
||||
} else {
|
||||
line_candidate = _line;
|
||||
continue;
|
||||
}
|
||||
} while (_line <= _cm.lineCount());
|
||||
return line_candidate;
|
||||
}(cm, line),
|
||||
// specific
|
||||
status: match[2].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function rearrange_list(cm, line) {
|
||||
const line_inferior = find_limit_inferior(cm, line);
|
||||
const line_superior = find_limit_superior(cm, line);
|
||||
|
||||
let last_p = null;
|
||||
let p;
|
||||
|
||||
for (let i=line_inferior; i<=line_superior; i++) {
|
||||
if (p = isNumberedList(cm, i)) {
|
||||
// rearrange numbers on the numbered list
|
||||
if (last_p) {
|
||||
if (p.level === last_p.level) {
|
||||
const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
|
||||
if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
|
||||
} else if (p.level > last_p.level) {
|
||||
if (p.n !== 1) {
|
||||
setNumber(cm, p.start, 1);
|
||||
}
|
||||
} else if (p.level < last_p.level) {
|
||||
const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
|
||||
if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
|
||||
}
|
||||
} else {
|
||||
if (p.n !== 1) setNumber(cm, p.start, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (p = (isNumberedList(cm, i) || isItemList(cm, i))) {
|
||||
// rearrange spacing levels in list
|
||||
if (last_p) {
|
||||
if (p.level > last_p.level) {
|
||||
if (p.level !== last_p.level + 1) {
|
||||
setLevel(cm, [p.start, p.end], last_p.level + 1, p.type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (p.level !== 0) {
|
||||
setLevel(cm, [p.start, p.end], 0, p.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
last_p = p;
|
||||
// we can process content block instead of line
|
||||
if (p) {
|
||||
i += (p.end - p.start);
|
||||
}
|
||||
}
|
||||
|
||||
function findLastAtLevel(_cm, line, line_limit_inf, level) {
|
||||
let p;
|
||||
do {
|
||||
line -= 1;
|
||||
if ((p = isNumberedList(_cm, line)) && p.level === level) {
|
||||
return p;
|
||||
}
|
||||
} while (line > line_limit_inf);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setLevel(_cm, range, level, type) {
|
||||
let content;
|
||||
let i;
|
||||
for (i=range[0]; i<=range[1]; i++) {
|
||||
content = cm.getLine(i).trimLeft();
|
||||
const n_spaces = function(_level, _line, _type) {
|
||||
let spaces = _level * 3;
|
||||
if (_line > 0) {
|
||||
spaces += _type === "numbered" ? 3 : 2;
|
||||
}
|
||||
return spaces;
|
||||
}(level, i - range[0], type);
|
||||
|
||||
content = " ".repeat(n_spaces) + content;
|
||||
cm.replaceRange(
|
||||
content,
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: _cm.getLine(i).length },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setNumber(_cm, line, level) {
|
||||
const content = _cm.getLine(line);
|
||||
const new_content = content.replace(/[0-9]+\./, level+".");
|
||||
cm.replaceRange(
|
||||
new_content,
|
||||
{ line: line, ch: 0 },
|
||||
{ line: line, ch: content.length },
|
||||
);
|
||||
}
|
||||
|
||||
function find_limit_inferior(_cm, _line) {
|
||||
let content;
|
||||
let p;
|
||||
let match;
|
||||
let line_candidate = _line;
|
||||
do {
|
||||
content = _cm.getLine(_line);
|
||||
p = isNumberedList(_cm, _line);
|
||||
match = /(\s+).*$/.exec(content);
|
||||
if (p) line_candidate = _line;
|
||||
if (!p || !match) break;
|
||||
_line -= 1;
|
||||
} while (_line >= 0);
|
||||
return line_candidate;
|
||||
}
|
||||
function find_limit_superior(_cm, _line) {
|
||||
let content;
|
||||
let p;
|
||||
let match;
|
||||
let line_candidate = _line;
|
||||
do {
|
||||
content = _cm.getLine(_line);
|
||||
p = isNumberedList(_cm, _line);
|
||||
match = /(\s+).*$/.exec(content);
|
||||
if (p) line_candidate = _line;
|
||||
if (!p || !match) break;
|
||||
_line += 1;
|
||||
} while (_line < _cm.lineCount());
|
||||
return line_candidate;
|
||||
}
|
||||
}
|
||||
|
||||
function swap(cm, from, to) {
|
||||
const from_content = cm.getRange(
|
||||
{ line: from[0], ch: 0 },
|
||||
{ line: from[1], ch: cm.getLine(from[1]).length },
|
||||
);
|
||||
const to_content = cm.getRange(
|
||||
{ line: to[0], ch: 0 },
|
||||
{ line: to[1], ch: cm.getLine(to[1]).length },
|
||||
);
|
||||
const cursor = cm.getCursor();
|
||||
|
||||
if (to[0] > from[0]) {
|
||||
// moving down
|
||||
cm.replaceRange(
|
||||
from_content,
|
||||
{ line: to[0], ch: 0 },
|
||||
{ line: to[1], ch: cm.getLine(to[1]).length },
|
||||
);
|
||||
cm.replaceRange(
|
||||
to_content,
|
||||
{ line: from[0], ch: 0 },
|
||||
{ line: from[1], ch: cm.getLine(from[1]).length },
|
||||
);
|
||||
cm.setCursor({
|
||||
line: cursor.line + (to[1] - to[0] + 1),
|
||||
ch: cursor.ch,
|
||||
});
|
||||
} else {
|
||||
// moving up
|
||||
cm.replaceRange(
|
||||
to_content,
|
||||
{ line: from[0], ch: 0 },
|
||||
{ line: from[1], ch: cm.getLine(from[1]).length },
|
||||
);
|
||||
cm.replaceRange(
|
||||
from_content,
|
||||
{ line: to[0], ch: 0 },
|
||||
{ line: to[1], ch: cm.getLine(to[1]).length },
|
||||
);
|
||||
cm.setCursor({
|
||||
line: cursor.line - (to[1] - to[0] + 1),
|
||||
ch: cursor.ch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function fold(cm, start) {
|
||||
cm.foldCode(start, null, "fold");
|
||||
}
|
||||
export function unfold(cm, start) {
|
||||
cm.foldCode(start, null, "unfold");
|
||||
}
|
||||
export function isFold(cm, start) {
|
||||
const line = start.line;
|
||||
const marks = cm.findMarks(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0));
|
||||
for (let i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/erlang/erlang.js";
|
||||
CodeMirror.__mode = "erlang";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/go/go.js";
|
||||
CodeMirror.__mode = "go";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/htmlmixed/htmlmixed.js";
|
||||
CodeMirror.__mode = "htmlmixed";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/javascript/javascript.js";
|
||||
CodeMirror.__mode = "javascript";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/jsx/jsx.js";
|
||||
CodeMirror.__mode = "jsx";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1 @@
|
||||
import "../../../lib/vendor/codemirror/keymap/sublime.js";
|
||||
@@ -0,0 +1 @@
|
||||
import "../../../lib/vendor/codemirror/keymap/vim.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/lua/lua.js";
|
||||
CodeMirror.__mode = "lua";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,313 @@
|
||||
import "../../../lib/vendor/codemirror/addon/mode/simple.js";
|
||||
import {
|
||||
org_cycle, org_shifttab, org_metaleft, org_metaright, org_meta_return, org_metaup,
|
||||
org_metadown, org_insert_todo_heading, org_shiftleft, org_shiftright, fold, unfold,
|
||||
isFold, org_set_fold, org_shiftmetaleft, org_shiftmetaright,
|
||||
} from "./emacs-org.js";
|
||||
|
||||
// TODO:
|
||||
// import { pathBuilder, dirname, currentShare } from "../../../helpers/";
|
||||
|
||||
const CodeMirror = window.CodeMirror;
|
||||
|
||||
CodeMirror.__mode = "orgmode";
|
||||
|
||||
CodeMirror.defineSimpleMode("orgmode", {
|
||||
start: [
|
||||
{ regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star", "header level1 org-todo", "header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"] },
|
||||
{ regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star", "header org-todo", "header org-done", "header org-priority", "header", "header void", "header comment"] },
|
||||
{ regex: /(\+[^\+]+\+)/, token: ["strikethrough"] },
|
||||
{ regex: /(\*[^\*]+\*)/, token: ["strong"] },
|
||||
{ regex: /(\/[^\/]+\/)/, token: ["em"] },
|
||||
{ regex: /(\_[^\_]+\_)/, token: ["link"] },
|
||||
{ regex: /(\~[^\~]+\~)/, token: ["comment"] },
|
||||
{ regex: /(\=[^\=]+\=)/, token: ["comment"] },
|
||||
{ regex: /\[\[[^\[\]]+\]\[[^\[\]]+\]\]/, token: "org-url" }, // links
|
||||
{ regex: /\[\[[^\[\]]+\]\]/, token: "org-image" }, // image
|
||||
{ regex: /\[[xX\s\-\_]\]/, token: "qualifier org-toggle" }, // checkbox
|
||||
{ regex: /\#\+(?:(BEGIN|begin))_[a-zA-Z]*/, token: "comment", next: "env", sol: true }, // comments
|
||||
{ regex: /:?[A-Z_]+\:.*/, token: "comment", sol: true }, // property drawers
|
||||
{ regex: /(\#\+[a-zA-Z_]*)(\:.*)/, token: ["keyword", "qualifier"], sol: true }, // environments
|
||||
{ regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"] },
|
||||
],
|
||||
env: [
|
||||
{ regex: /\#\+(?:(END|end))_[a-zA-Z]*/, token: "comment", next: "start", sol: true },
|
||||
{ regex: /.*/, token: "comment" },
|
||||
],
|
||||
});
|
||||
CodeMirror.registerHelper("fold", "orgmode", function(cm, start) {
|
||||
// init
|
||||
const levelToMatch = headerLevel(start.line);
|
||||
|
||||
// no folding needed
|
||||
if (levelToMatch === null) return;
|
||||
|
||||
// find folding limits
|
||||
const lastLine = cm.lastLine();
|
||||
let end = start.line;
|
||||
while (end < lastLine) {
|
||||
end += 1;
|
||||
const level = headerLevel(end);
|
||||
if (level && level <= levelToMatch) {
|
||||
end = end - 1;
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
|
||||
to: CodeMirror.Pos(end, cm.getLine(end).length),
|
||||
};
|
||||
|
||||
function headerLevel(lineNo) {
|
||||
const line = cm.getLine(lineNo);
|
||||
const match = /^\*+/.exec(line);
|
||||
if (match && match.length === 1 && /header/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)))) {
|
||||
return match[0].length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
|
||||
return mode.name === "orgmode" ? true : false;
|
||||
}, function(cm, start) {
|
||||
const drawer = isBeginningOfADrawer(start.line);
|
||||
if (drawer === false) return;
|
||||
|
||||
// find folding limits
|
||||
const lastLine = cm.lastLine();
|
||||
let end = start.line;
|
||||
while (end < lastLine) {
|
||||
end += 1;
|
||||
if (isEndOfADrawer(end)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
|
||||
to: CodeMirror.Pos(end, cm.getLine(end).length),
|
||||
};
|
||||
|
||||
function isBeginningOfADrawer(lineNo) {
|
||||
const line = cm.getLine(lineNo);
|
||||
const match = /^\:.*\:$/.exec(line);
|
||||
if (match && match.length === 1 && match[0] !== ":END:") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isEndOfADrawer(lineNo) {
|
||||
const line = cm.getLine(lineNo);
|
||||
return line.trim() === ":END:" ? true : false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
CodeMirror.registerHelper("orgmode", "init", (editor, fn) => {
|
||||
editor.setOption("extraKeys", {
|
||||
"Tab": (cm) => org_cycle(cm),
|
||||
"Shift-Tab": (cm) => fn("shifttab", org_shifttab(cm)),
|
||||
"Alt-Left": (cm) => org_metaleft(cm),
|
||||
"Alt-Right": (cm) => org_metaright(cm),
|
||||
"Alt-Enter": (cm) => org_meta_return(cm),
|
||||
"Alt-Up": (cm) => org_metaup(cm),
|
||||
"Alt-Down": (cm) => org_metadown(cm),
|
||||
"Shift-Alt-Left": (cm) => org_shiftmetaleft(cm),
|
||||
"Shift-Alt-Right": (cm) => org_shiftmetaright(cm),
|
||||
"Shift-Alt-Enter": (cm) => org_insert_todo_heading(cm),
|
||||
"Shift-Left": (cm) => org_shiftleft(cm),
|
||||
"Shift-Right": (cm) => org_shiftright(cm),
|
||||
});
|
||||
fn("shifttab", org_set_fold(editor));
|
||||
|
||||
editor.on("mousedown", toggleHandler);
|
||||
editor.on("touchstart", toggleHandler);
|
||||
editor.on("gutterClick", foldLine);
|
||||
|
||||
// fold everything except headers by default
|
||||
editor.operation(function() {
|
||||
for (let i = 0; i < editor.lineCount(); i++) {
|
||||
if (/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false) {
|
||||
fold(editor, CodeMirror.Pos(i, 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
return CodeMirror.orgmode.destroy.bind(this, editor);
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("orgmode", "destroy", (editor) => {
|
||||
editor.off("mousedown", toggleHandler);
|
||||
editor.off("touchstart", toggleHandler);
|
||||
editor.off("gutterClick", foldLine);
|
||||
});
|
||||
|
||||
function foldLine(cm, line) {
|
||||
const cursor = { line: line, ch: 0 };
|
||||
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
|
||||
}
|
||||
|
||||
|
||||
let widgets = [];
|
||||
function toggleHandler(cm, e) {
|
||||
const position = cm.coordsChar({
|
||||
left: e.clientX || (e.targetTouches && e.targetTouches[0].clientX),
|
||||
top: e.clientY || (e.targetTouches && e.targetTouches[0].clientY),
|
||||
}, "page");
|
||||
const token = cm.getTokenAt(position);
|
||||
|
||||
_disableSelection();
|
||||
if (/org-level-star/.test(token.type)) {
|
||||
_preventIfShould();
|
||||
_foldHeadline();
|
||||
_disableSelection();
|
||||
} else if (/org-toggle/.test(token.type)) {
|
||||
_preventIfShould();
|
||||
_toggleCheckbox();
|
||||
_disableSelection();
|
||||
} else if (/org-todo/.test(token.type)) {
|
||||
_preventIfShould();
|
||||
_toggleTodo();
|
||||
_disableSelection();
|
||||
} else if (/org-done/.test(token.type)) {
|
||||
_preventIfShould();
|
||||
_toggleDone();
|
||||
_disableSelection();
|
||||
} else if (/org-priority/.test(token.type)) {
|
||||
_preventIfShould();
|
||||
_togglePriority();
|
||||
_disableSelection();
|
||||
} else if (/org-url/.test(token.type)) {
|
||||
_disableSelection();
|
||||
_navigateLink();
|
||||
} else if (/org-image/.test(token.type)) {
|
||||
_disableSelection();
|
||||
_toggleImageWidget();
|
||||
}
|
||||
|
||||
function _preventIfShould() {
|
||||
if ("ontouchstart" in window) e.preventDefault();
|
||||
}
|
||||
function _disableSelection() {
|
||||
cm.on("beforeSelectionChange", _onSelectionChangeHandler);
|
||||
function _onSelectionChangeHandler(cm, obj) {
|
||||
obj.update([{
|
||||
anchor: position,
|
||||
head: position,
|
||||
}]);
|
||||
cm.off("beforeSelectionChange", _onSelectionChangeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function _foldHeadline() {
|
||||
const line = position.line;
|
||||
if (line >= 0) {
|
||||
const cursor = { line: line, ch: 0 };
|
||||
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
function _toggleCheckbox() {
|
||||
const line = position.line;
|
||||
const content = cm.getRange(
|
||||
{ line: line, ch: token.start },
|
||||
{ line: line, ch: token.end },
|
||||
);
|
||||
const new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]";
|
||||
cm.replaceRange(
|
||||
new_content,
|
||||
{ line: line, ch: token.start },
|
||||
{ line: line, ch: token.end },
|
||||
);
|
||||
}
|
||||
|
||||
function _toggleTodo() {
|
||||
const line = position.line;
|
||||
cm.replaceRange(
|
||||
"DONE",
|
||||
{ line: line, ch: token.start },
|
||||
{ line: line, ch: token.end },
|
||||
);
|
||||
}
|
||||
|
||||
function _toggleDone() {
|
||||
const line = position.line;
|
||||
cm.replaceRange(
|
||||
"TODO",
|
||||
{ line: line, ch: token.start },
|
||||
{ line: line, ch: token.end },
|
||||
);
|
||||
}
|
||||
|
||||
function _togglePriority() {
|
||||
const PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "];
|
||||
const line = position.line;
|
||||
const content = cm.getRange({ line: line, ch: token.start }, { line: line, ch: token.end });
|
||||
const new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1];
|
||||
cm.replaceRange(
|
||||
new_content,
|
||||
{ line: line, ch: token.start },
|
||||
{ line: line, ch: token.end },
|
||||
);
|
||||
}
|
||||
|
||||
function _toggleImageWidget() {
|
||||
const exist = !!widgets
|
||||
.filter((line) => line === position.line)[0];
|
||||
|
||||
if (exist === false) {
|
||||
if (!token.string.match(/\[\[(.*)\]\]/)) return null;
|
||||
const $node = _buildImage(RegExp.$1);
|
||||
const widget = cm.addLineWidget(position.line, $node, { coverGutter: false });
|
||||
widgets.push(position.line);
|
||||
$node.addEventListener("click", closeWidget);
|
||||
|
||||
function closeWidget() {
|
||||
widget.clear();
|
||||
$node.removeEventListener("click", closeWidget);
|
||||
widgets = widgets.filter((line) => line !== position.line);
|
||||
}
|
||||
}
|
||||
function _buildImage(src) {
|
||||
const $el = document.createElement("div");
|
||||
const $img = document.createElement("img");
|
||||
|
||||
if (/^https?\:\/\//.test(src)) {
|
||||
$img.src = src;
|
||||
} else {
|
||||
const root_path = dirname(window.location.pathname.replace(/^\/view/, ""));
|
||||
const img_path = src;
|
||||
$img.src = "/api/files/cat?path="+encodeURIComponent(pathBuilder(root_path, img_path));
|
||||
}
|
||||
$el.appendChild($img);
|
||||
return $el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _navigateLink() {
|
||||
token.string.match(/\[\[(.*?)\]\[/);
|
||||
const link = RegExp.$1;
|
||||
if (!link) return;
|
||||
|
||||
let open = "_blank";
|
||||
const isMobile = screen.availWidth < screen.availHeight;
|
||||
if (!document.querySelector(".component_fab img.component_icon[alt=\"save\"]")) {
|
||||
open = "_self";
|
||||
} else if (isMobile) {
|
||||
open = "_self";
|
||||
}
|
||||
|
||||
if (/^https?\:\/\//.test(link)) {
|
||||
window.open(link, open);
|
||||
} else {
|
||||
const root_path = dirname(window.location.pathname.replace(/^\/view/, ""));
|
||||
const share = currentShare();
|
||||
const url = share ? "/view"+pathBuilder(root_path, link)+"?share="+share : "/view"+pathBuilder(root_path, link);
|
||||
window.open(url, open);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/perl/perl.js";
|
||||
CodeMirror.__mode = "perl";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/php/php.js";
|
||||
CodeMirror.__mode = "php";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/properties/properties.js";
|
||||
CodeMirror.__mode = "properties";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/python/python.js";
|
||||
CodeMirror.__mode = "python";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/r/r.js";
|
||||
CodeMirror.__mode = "r";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/ruby/ruby.js";
|
||||
CodeMirror.__mode = "ruby";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/rust/rust.js";
|
||||
CodeMirror.__mode = "rust";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/sass/sass.js";
|
||||
CodeMirror.__mode = "sass";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/shell/shell.js";
|
||||
CodeMirror.__mode = "shell";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/sparql/sparql.js";
|
||||
CodeMirror.__mode = "sparql";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/spreadsheet/spreadsheet.js";
|
||||
CodeMirror.__mode = "spreadsheet";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/sql/sql.js";
|
||||
CodeMirror.__mode = "sql";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/stex/stex.js";
|
||||
CodeMirror.__mode = "stex";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1 @@
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/xml/xml.js";
|
||||
CodeMirror.__mode = "xml";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,4 @@
|
||||
import "../../../lib/vendor/codemirror/mode/gfm/gfm.js";
|
||||
import "../../../lib/vendor/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js";
|
||||
CodeMirror.__mode = "yaml-frontmatter";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,3 @@
|
||||
import "../../../lib/vendor/codemirror/mode/yaml/yaml.js";
|
||||
CodeMirror.__mode = "yaml";
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function(editor) {
|
||||
CodeMirror.orgmode.init(editor, (key, value) => {
|
||||
if (key === "shifttab") {
|
||||
// org_shifttab(this.state.editor)
|
||||
// this.props.onFoldChange(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -167,6 +167,9 @@ export default function(render, { mime }) {
|
||||
hls.attachMedia($video);
|
||||
}),
|
||||
rxjs.mergeMap(() => rxjs.fromEvent($video, "loadeddata")),
|
||||
// rxjs.tap(() => renderMenubar(buildMenubar(
|
||||
// menubarDownload(),
|
||||
// ))),
|
||||
rxjs.mergeMap(() => {
|
||||
const $loader = qs($page, ".loader");
|
||||
$loader.replaceChildren(createElement(`<img style="height:170px;cursor:pointer;filter:brightness(0.5) invert(1);" src="${ICON.PLAY}" />`));
|
||||
@@ -177,9 +180,6 @@ export default function(render, { mime }) {
|
||||
setSeek(0);
|
||||
return rxjs.fromEvent($loader, "click").pipe(rxjs.mapTo($loader));
|
||||
}),
|
||||
// rxjs.tap(() => renderMenubar(buildMenubar(
|
||||
// menubarDownload(),
|
||||
// ))),
|
||||
rxjs.tap(($loader) => {
|
||||
$loader.classList.add("hidden")
|
||||
const $control = qs($page, ".videoplayer_control");
|
||||
|
||||
@@ -16,7 +16,7 @@ export function getFile$() {
|
||||
export function saveFile$() {
|
||||
return rxjs.pipe(
|
||||
rxjs.delay(2000),
|
||||
rxjs.tap((content) => console.log("SAVED", content)),
|
||||
rxjs.tap((content) => console.log("SAVED")),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import rxjs from "../../lib/rx.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
|
||||
export function fileOptions(path) {
|
||||
return ajax({
|
||||
url: `/api/files/cat?path=${path}`,
|
||||
method: "OPTIONS",
|
||||
}).pipe(rxjs.map((res) => res.responseHeaders.allow.replace(/\r/, "").split(", ")));
|
||||
}
|
||||
Reference in New Issue
Block a user