import React from "react"; import { StickyContainer, Sticky } from "react-sticky"; import { Modal, NgIf, Icon, Dropdown, DropdownButton, DropdownList, DropdownItem, Input } from "../../components/"; import { extractEvents, extractTodos } from "../../helpers/org"; import { leftPad } from "../../helpers/common"; import { debounce, randomString } from "../../helpers/"; import { t } from "../../locales/"; import "./org_viewer.scss"; export class OrgEventsViewer extends React.Component { shouldComponentUpdate(nextProps) { if (this.props.content !== nextProps.content) return true; if (this.props.isActive !== nextProps.isActive) return true; return false; } render() { const headlines = this.props.isActive ? extractEvents(this.props.content) : []; return ( ); } } export class OrgTodosViewer extends React.Component { shouldComponentUpdate(nextProps) { if (this.props.content !== nextProps.content) return true; if (this.props.isActive !== nextProps.isActive) return true; return false; } render() { const headlines = this.props.isActive ? extractTodos(this.props.content) : []; return ( ); } } class OrgViewer extends React.Component { constructor(props) { super(props); this.state = { headlines: this.buildHeadlines(props.headlines), content: props.content, search: "", _: null, }; this.rerender = () => this.setState({ _: randomString() }); this.findResults = debounce(this.findResults.bind(this), 150); } UNSAFE_componentWillReceiveProps(props) { this.setState({ headlines: this.buildHeadlines(props.headlines), content: props.content, }); } buildHeadlines(headlines) { return headlines .reduce((acc, headline) => { if (!acc[headline["key"]]) { acc[headline["key"]] = []; } acc[headline["key"]].push(headline); return acc; }, {}); } onChange(i, j, state) { const headlines = { ...this.state.headlines }; headlines[Object.keys(this.state.headlines)[i]][j].status = state; this.setState({ headlines: this.state.headlines, }); } onTaskUpdate(type, line, value) { const content = this.state.content.split("\n"); let head_line; let item_line; let head_status; let deadline_line; let scheduled_line; let insertion_line; switch (type) { case "status": content[line] = content[line].replace(/^(\*+\s)[A-Z]{3,}(\s.*)$/, "$1"+value+"$2"); break; case "subtask": if (value === "DONE") { content[line] = content[line].replace(/\[.\]/, "[X]"); } else { content[line] = content[line].replace(/\[.\]/, "[ ]"); } break; case "existing_scheduled": [head_line, head_status, item_line] = line; content[item_line] = content[item_line].replace(/SCHEDULED: <.*?>\s*/, value ? "SCHEDULED: "+orgdate(value)+" " : ""); this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { if (todo.line === head_line) { if (value) todo.scheduled.timestamp = new Date(value).toISOString(); else todo.scheduled = null; } return todo; }); this.setState({ headlines: this.state.headlines }); break; case "existing_deadline": [head_line, head_status, item_line] = line; content[item_line] = content[item_line].replace(/DEADLINE: <.*?>\s*/, value ? "DEADLINE: "+orgdate(value) : ""); this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { if (todo.line === head_line) { if (value) todo.deadline.timestamp = new Date(value).toISOString(); else todo.deadline = null; } return todo; }); this.setState({ headlines: this.state.headlines }); break; case "new_scheduled": [head_line, head_status, deadline_line] = line; if (deadline_line !== null) { insertion_line = deadline_line; content[deadline_line] = "SCHEDULED: "+orgdate(value)+" "+content[deadline_line]; } else { insertion_line = head_line + 1; if (content[insertion_line] === "" && content[insertion_line + 1] === "") { content[insertion_line] = "SCHEDULED: "+orgdate(value); } else { content.splice( insertion_line, 0, "SCHEDULED: "+orgdate(value), ); } } this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { if (todo.line === head_line) { todo.scheduled = { line: insertion_line, keyword: "SCHEDULED", active: true, range: null, repeat: null, timestamp: new Date(value).toISOString(), }; } return todo; }); this.setState({ headlines: this.state.headlines }); break; case "new_deadline": [head_line, head_status, scheduled_line] = line; if (scheduled_line !== null) { insertion_line = scheduled_line; content[scheduled_line] = content[scheduled_line]+" DEADLINE: "+orgdate(value); } else { insertion_line = head_line + 1; if (content[insertion_line] === "" && content[insertion_line + 1] === "") { content[insertion_line] = "DEADLINE: "+orgdate(value); } else { content.splice( insertion_line, 0, "DEADLINE: "+orgdate(value), ); } this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { if (todo.line === head_line) { todo.deadline = { line: insertion_line, keyword: "DEADLINE", active: true, range: null, repeat: null, timestamp: new Date(value).toISOString(), }; } return todo; }); this.setState({ headlines: this.state.headlines }); } break; } this.setState({ content: content.join("\n") }); function orgdate(_date) { const date = new Date(_date); return "<"+date.getFullYear()+"-"+leftPad((date.getMonth() + 1).toString(), 2)+"-"+leftPad(date.getDate().toString(), 2)+" "+day(date.getDay())+">"; function day(n) { switch (navigator.language.split("-")[0]) { case "de": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"][n]; break; default: return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][n]; } } } } navigate(line) { this.props.goTo(line); this.onQuit(); } onQuit() { this.props.onUpdate(this.state.content); this.props.onQuit(); } componentDidMount() { window.addEventListener("resize", this.rerender); } componentWillUnmount() { window.removeEventListener("resize", this.rerender); } search(terms) { this.setState({ search: terms }, () => { this.findResults(terms); }); } findResults(terms) { let headlines = this.props.headlines; if (terms) { headlines = this.props.headlines.filter((headline) => { const keywords = terms.split(" "); const head = function() { let str = " "; str += headline["status"] + " "; str += headline["title"] + " "; str += headline.tags.map((tag) => "#"+tag).join(" ") + " "; str += headline.scheduled ? "scheduled "+headline.scheduled.timestamp + " ": ""; str += headline.deadline ? "deadline "+headline.deadline.timestamp + " ": ""; str += headline.priority ? "priority #"+headline.priority+" " : ""; str += headline.is_overdue ? "overdue " : ""; str += headline.tasks.map((task) => task.title).join(" ")+ " "; return str; }(headline); return keywords.filter((keyword) => new RegExp(" "+keyword, "i").test(head)).length === keywords.length ? true : false; }); } this.setState({ headlines: this.buildHeadlines(headlines) }); } render() { return (

{this.props.title}

0}>
{ t("empty") } 0}> 750 ? 545 : window.innerHeight - 202 }}> { Object.keys(this.state.headlines).map((list, i) => { return (
{ ({ style }) => { return (

{list} {this.state.headlines[list].length}

); } }
{ this.state.headlines[list].map((headline, j) => { return ( ); }) }
); }) }
); } } class Headline extends React.Component { constructor(props) { super(props); this.state = { status: props.todo_status, properties: false, }; } onMenuAction(key) { if (key === "navigate") { this.props.goTo(); } else if (key === "properties") { this.setState({ properties: !this.state.properties }); } } onStatusToggle() { if (!this.props.todo_status) return; const new_status = this.state.status === "todo" ? "done" : "todo"; this.setState({ status: new_status }); const new_status_label = function(new_status, initial_status, initial_keyword) { if (new_status === initial_status) return initial_keyword; return new_status === "todo" ? "TODO" : "DONE"; }(new_status, this.props.todo_status, this.props.status); this.props.onTaskUpdate("status", this.props.line, new_status_label); } onTimeSet(keyword, existing, value) { if (existing === true) { this.props.onTaskUpdate( "existing_"+keyword, [ this.props.line, this.props.sortKey, this.props[keyword].line, ], value, ); } else { const opposite_keyword = keyword === "scheduled" ? "deadline" : "scheduled"; this.props.onTaskUpdate( "new_"+keyword, [ this.props.line, this.props.sortKey, this.props[opposite_keyword] && this.props[opposite_keyword].line || null, ], value, ); } } render() { const dateInput = (obj) => { if (!obj || !obj.timestamp) return ""; const d = new Date(obj.timestamp); return d.getFullYear()+"-"+leftPad((d.getMonth() + 1).toString(), 2)+"-"+leftPad(d.getDate().toString(), 2); }; return (
{this.props.title}
{ this.props.tags.map((tag, i) => { return ( {tag} ); }) }
{ t("Navigate") } { t("Properties") }
0 && this.state.status === "todo" && this.props.type === "todos"} className="subtask_container"> { this.props.tasks.map((task, i) => { return ( ); }) }
); } } class Subtask extends React.Component { constructor(props) { super(props); this.state = this.calculateState(); } calculateState() { return { checked: this.props.status === "DONE" }; } updateState(e) { const checked = e.target.checked; this.setState({ checked: checked }, () => { // We don't want the interface to feel laggy while a task is beeing updated. Updating // the content and reparsing the result is an expensive operation, this makes it feel // like a piece of cake window.setTimeout(() => { window.requestAnimationFrame(() => { this.props.onStatusChange(checked ? "DONE" : "TODO"); }); }, 0); }); } render() { return (
); } }