feature (org): cool stuff for org mode users ;)

This commit is contained in:
Mickael KERJEAN
2018-04-24 22:58:28 +10:00
parent e31cbc13e9
commit 259061f5d8
34 changed files with 2570 additions and 289 deletions

View File

@ -48,5 +48,5 @@ Note that on the FTP and sFTP, sessions connections aren't destroyed on every re
Nuage is an open source software with its source code available under the AGPL license. Commercial license and support is available upon request, contact me for details: mickael@kerjean.me
# Credits
- Icons from www.flaticon.com
- Folks developing awesome [libraries](https://github.com/mickael-kerjean/nuage/blob/master/package.json) as Nuage is just butter and cream on top.
- Iconography: www.flaticon.com, fontawesome.com and material.io
- Folks developing awesome [libraries](https://github.com/mickael-kerjean/nuage/blob/master/package.json)

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
aria-hidden="true"
data-prefix="fas"
data-icon="arrow-alt-to-bottom"
role="img"
viewBox="0 0 450 512"
class="svg-inline--fa fa-arrow-alt-to-bottom fa-w-12 fa-9x"
version="1.1"
id="svg7697"
sodipodi:docname="arrow-down-double.svg"
inkscape:version="0.92.2 2405546, 2018-03-11"
width="450"
height="512">
<metadata
id="metadata7703">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7701" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview7699"
showgrid="false"
inkscape:zoom="1.3037281"
inkscape:cx="160.34077"
inkscape:cy="298.0013"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="layer3" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="lines"
style="display:inline">
<path
d="m 102.77376,57.71175 v 259.88716 h -79.7 c -17.7999996,0 -26.9165146,14.53938 -14.0999996,30.87906 L 123.17376,494.07066 c 7.62888,9.726 19.67112,9.726 27.3,0 l 114.2,-145.59269 c 12.81652,-16.33968 3.7,-30.87906 -14.1,-30.87906 h -79.8 V 57.71175 c 0,-18.58333 -13.5475,-27.48437 -24,-33.53383 -6.1592,-3.56467 -20,0 -20,0 -13.3,0 -23.86429,14.95148 -24,33.53383 z"
class=""
id="path7695"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.18205023"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsssssscsass" />
<path
sodipodi:nodetypes="sssssssss"
inkscape:connector-curvature="0"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
id="path3632"
class=""
d="M 417.23298,74.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z" />
<path
d="M 417.23298,172.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z"
class=""
id="path3662"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssss" />
<path
sodipodi:nodetypes="sssssssss"
inkscape:connector-curvature="0"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
id="path3664"
class=""
d="M 417.23298,270.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
aria-hidden="true"
data-prefix="fas"
data-icon="arrow-alt-to-bottom"
role="img"
viewBox="0 0 450 512"
class="svg-inline--fa fa-arrow-alt-to-bottom fa-w-12 fa-9x"
version="1.1"
id="svg7697"
sodipodi:docname="arrow-down.svg"
inkscape:version="0.92.2 2405546, 2018-03-11"
width="450"
height="512">
<metadata
id="metadata7703">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7701" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview7699"
showgrid="false"
inkscape:zoom="1.3037281"
inkscape:cx="160.34077"
inkscape:cy="298.0013"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="layer3" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="lines"
style="display:inline">
<path
d="m 102.77376,57.71175 v 259.88716 h -79.7 c -17.7999996,0 -26.9165146,14.53938 -14.0999996,30.87906 L 123.17376,494.07066 c 7.62888,9.726 19.67112,9.726 27.3,0 l 114.2,-145.59269 c 12.81652,-16.33968 3.7,-30.87906 -14.1,-30.87906 h -79.8 V 57.71175 c 0,-18.58333 -13.5475,-27.48437 -24,-33.53383 -6.1592,-3.56467 -20,0 -20,0 -13.3,0 -23.86429,14.95148 -24,33.53383 z"
class=""
id="path7695"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.18205023"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsssssscsass" />
<path
sodipodi:nodetypes="sssssssss"
inkscape:connector-curvature="0"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
id="path3632"
class=""
d="M 417.23298,74.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z" />
<path
d="M 367.87619,172.09757 H 234.71087 c -13.02278,0 -23.49976,-6.7 -23.49976,-20 v -12 c 0,-13.3 10.47698,-20 23.49976,-20 h 133.16532 c 13.02279,0 23.49976,6.7 23.49976,20 v 12 c 0,13.3 -10.47697,20 -23.49976,20 z"
class=""
id="path3662"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:0.98952353"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssss" />
<path
sodipodi:nodetypes="sssssssss"
inkscape:connector-curvature="0"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:0.79045671"
id="path3664"
class=""
d="m 311.18259,270.09757 h -84.97576 c -8.31013,0 -14.99572,-6.7 -14.99572,-20 v -12 c 0,-13.3 6.68559,-20 14.99572,-20 h 84.97576 c 8.31013,0 14.99571,6.7 14.99571,20 v 12 c 0,13.3 -6.68558,20 -14.99571,20 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
aria-hidden="true"
data-prefix="fas"
data-icon="arrow-alt-to-bottom"
role="img"
viewBox="0 0 450 512"
class="svg-inline--fa fa-arrow-alt-to-bottom fa-w-12 fa-9x"
version="1.1"
id="svg7697"
sodipodi:docname="arrow-up-double.svg"
inkscape:version="0.92.2 2405546, 2018-03-11"
width="450"
height="512">
<metadata
id="metadata7703">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7701" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview7699"
showgrid="false"
inkscape:zoom="1.3037281"
inkscape:cx="160.34077"
inkscape:cy="298.0013"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="layer3" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="lines"
style="display:inline">
<path
d="M 102.77376,466.24703 V 206.35987 h -79.7 c -17.7999996,0 -26.9165146,-14.53938 -14.0999996,-30.87906 L 123.17376,29.888122 c 7.62888,-9.726 19.67112,-9.726 27.3,0 l 114.2,145.592688 c 12.81652,16.33968 3.7,30.87906 -14.1,30.87906 h -79.8 v 259.88716 c 0,18.58333 -13.5475,27.48437 -24,33.53383 -6.1592,3.56467 -20,0 -20,0 -13.3,0 -23.86429,-14.95148 -24,-33.53383 z"
class=""
id="path7695"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.18205023"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsssssscsass" />
<path
sodipodi:nodetypes="sssssssss"
inkscape:connector-curvature="0"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
id="path3632"
class=""
d="M 417.23298,302.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z" />
<path
d="M 417.23298,400.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z"
class=""
id="path3662"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssss" />
<path
sodipodi:nodetypes="sssssssss"
inkscape:connector-curvature="0"
style="fill:#f2f2f2;fill-opacity:1;stroke-width:1.13474083"
id="path3664"
class=""
d="M 417.23298,498.09757 H 242.11439 c -17.12557,0 -30.90328,-6.7 -30.90328,-20 v -12 c 0,-13.3 13.77771,-20 30.90328,-20 h 175.11859 c 17.12557,0 30.90327,6.7 30.90327,20 v 12 c 0,13.3 -13.7777,20 -30.90327,20 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
fill="#000000"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg855"
sodipodi:docname="arrow_right.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata861">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs859" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview857"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="8.8474576"
inkscape:cy="12"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg855" />
<path
d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"
id="path851"
style="fill:#000000;fill-opacity:0.53333336" />
<path
d="M0-.25h24v24H0z"
fill="none"
id="path853" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 448 512"
version="1.1"
id="svg5212"
sodipodi:docname="calendar.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata5218">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5216" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview5214"
showgrid="false"
inkscape:zoom="1.3037281"
inkscape:cx="61.388101"
inkscape:cy="240.15328"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg5212" />
<path
d="M 400,64 H 352 V 12 C 352,5.4 346.6,0 340,0 h -40 c -6.6,0 -12,5.4 -12,12 V 64 H 160 V 12 C 160,5.4 154.6,0 148,0 H 108 C 101.4,0 96,5.4 96,12 V 64 H 48 C 21.5,64 0,85.5 0,112 v 352 c 0,26.5 21.5,48 48,48 h 352 c 26.5,0 48,-21.5 48,-48 V 112 C 448,85.5 426.5,64 400,64 Z m -2,404 H 50 c -3.3,0 -6.022147,-2.70007 -6,-6 V 154 h 360 v 308 c 0,3.3 -2.7,6 -6,6 z"
id="path5210"
style="fill:#000000;fill-opacity:0.53333336"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scssssccsssscsssssssssssccss" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 448 512"
version="1.1"
id="svg5212"
sodipodi:docname="calendar_white.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata5218">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5216" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview5214"
showgrid="false"
inkscape:zoom="1.3037281"
inkscape:cx="224.38221"
inkscape:cy="240.15328"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg5212" />
<path
d="M 400,64 H 352 V 12 C 352,5.4 346.6,0 340,0 h -40 c -6.6,0 -12,5.4 -12,12 V 64 H 160 V 12 C 160,5.4 154.6,0 148,0 H 108 C 101.4,0 96,5.4 96,12 V 64 H 48 C 21.5,64 0,85.5 0,112 v 352 c 0,26.5 21.5,48 48,48 h 352 c 26.5,0 48,-21.5 48,-48 V 112 C 448,85.5 426.5,64 400,64 Z m -2,404 H 50 c -3.3,0 -6.022147,-2.70007 -6,-6 V 154 h 360 v 308 c 0,3.3 -2.7,6 -6,6 z"
id="path5210"
style="fill:#f2f2f2;fill-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scssssccsssscsssssssssssccss" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

100
client/assets/img/close.svg Normal file
View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 51.976 51.976"
style="enable-background:new 0 0 51.976 51.976;"
xml:space="preserve"
width="512px"
height="512px"
sodipodi:docname="close.svg"
inkscape:version="0.92.2 2405546, 2018-03-11"><metadata
id="metadata1544"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs1542" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview1540"
showgrid="false"
inkscape:zoom="1.84375"
inkscape:cx="211.15042"
inkscape:cy="276.97144"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="g1507" />
<g
id="g1507">
<path
d="m 36.241,36.241 c -0.781,0.781 -2.047,0.781 -2.828,0 l -7.425,-7.425 -7.778,7.778 c -0.781,0.781 -2.047,0.781 -2.828,0 -0.781,-0.781 -0.781,-2.047 0,-2.828 l 7.778,-7.778 -7.425,-7.425 c -0.781,-0.781 -0.781,-2.048 0,-2.828 0.781,-0.781 2.047,-0.781 2.828,0 l 7.425,7.425 7.071,-7.071 c 0.781,-0.781 2.047,-0.781 2.828,0 0.781,0.781 0.781,2.047 0,2.828 l -7.071,7.071 7.425,7.425 c 0.781,0.781 0.781,2.047 0,2.828 z"
id="path1505"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:0.53333336"
sodipodi:nodetypes="sscssscscscssscss" />
</g>
<g
id="g1509">
</g>
<g
id="g1511">
</g>
<g
id="g1513">
</g>
<g
id="g1515">
</g>
<g
id="g1517">
</g>
<g
id="g1519">
</g>
<g
id="g1521">
</g>
<g
id="g1523">
</g>
<g
id="g1525">
</g>
<g
id="g1527">
</g>
<g
id="g1529">
</g>
<g
id="g1531">
</g>
<g
id="g1533">
</g>
<g
id="g1535">
</g>
<g
id="g1537">
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 384 512"
version="1.1"
id="svg5210"
sodipodi:docname="deadline.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata5216">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5214" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="940"
inkscape:window-height="1027"
id="namedview5212"
showgrid="false"
inkscape:zoom="0.4609375"
inkscape:cx="-11.932203"
inkscape:cy="256"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg5210" />
<path
d="M360 64c13.255 0 24-10.745 24-24V24c0-13.255-10.745-24-24-24H24C10.745 0 0 10.745 0 24v16c0 13.255 10.745 24 24 24 0 90.965 51.016 167.734 120.842 192C75.016 280.266 24 357.035 24 448c-13.255 0-24 10.745-24 24v16c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24 0-90.965-51.016-167.734-120.842-192C308.984 231.734 360 154.965 360 64zM192 208c-57.787 0-104-66.518-104-144h208c0 77.945-46.51 144-104 144z"
id="path5208"
style="fill:#000000;fill-opacity:0.2" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
aria-hidden="true"
data-prefix="fas"
data-icon="arrow-alt-to-bottom"
role="img"
viewBox="0 0 384 512"
class="svg-inline--fa fa-arrow-alt-to-bottom fa-w-12 fa-9x"
version="1.1"
id="svg7697"
sodipodi:docname="download_white.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata7703">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7701" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview7699"
showgrid="false"
inkscape:zoom="1.3037281"
inkscape:cx="-4.9544349"
inkscape:cy="248.41386"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg7697" />
<path
d="M 360,460 H 24 C 10.7,460 0,453.3 0,440 v -12 c 0,-13.3 10.7,-20 24,-20 h 336 c 13.3,0 24,6.7 24,20 v 12 c 0,13.3 -10.7,20 -24,20 z M 158,56 V 242 H 58.3 c -17.8,0 -28.174568,11.17176 -14.1,22.1 l 134.2,104.2 c 8.37772,6.50491 18.92228,6.50491 27.3,0 L 339.9,264.1 C 353.97457,253.17176 343.6,242 325.8,242 H 226 V 56 c 0,-13.3 -13.5475,-19.670431 -24,-24 -6.1592,-2.551223 -20,0 -20,0 -13.3,0 -23.86429,10.700692 -24,24 z"
class=""
id="path7695"
style="fill:#f2f2f2;fill-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssssscsssssscsass" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,37 +1,98 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 51.976 51.976" style="enable-background:new 0 0 51.976 51.976;" xml:space="preserve" width="512px" height="512px">
<g>
<path d="M44.373,7.603c-10.137-10.137-26.632-10.138-36.77,0c-10.138,10.138-10.137,26.632,0,36.77s26.632,10.138,36.77,0 C54.51,34.235,54.51,17.74,44.373,7.603z M36.241,36.241c-0.781,0.781-2.047,0.781-2.828,0l-7.425-7.425l-7.778,7.778 c-0.781,0.781-2.047,0.781-2.828,0c-0.781-0.781-0.781-2.047,0-2.828l7.778-7.778l-7.425-7.425c-0.781-0.781-0.781-2.048,0-2.828 c0.781-0.781,2.047-0.781,2.828,0l7.425,7.425l7.071-7.071c0.781-0.781,2.047-0.781,2.828,0c0.781,0.781,0.781,2.047,0,2.828 l-7.071,7.071l7.425,7.425C37.022,34.194,37.022,35.46,36.241,36.241z" fill="#6f6f6f"/>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 51.976 51.976"
style="enable-background:new 0 0 51.976 51.976;"
xml:space="preserve"
width="512px"
height="512px"
sodipodi:docname="error.svg"
inkscape:version="0.92.2 2405546, 2018-03-11"><metadata
id="metadata1544"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs1542" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview1540"
showgrid="false"
inkscape:zoom="1.84375"
inkscape:cx="211.15042"
inkscape:cy="276.97144"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="g1507" />
<g
id="g1507">
<path
d="M44.373,7.603c-10.137-10.137-26.632-10.138-36.77,0c-10.138,10.138-10.137,26.632,0,36.77s26.632,10.138,36.77,0 C54.51,34.235,54.51,17.74,44.373,7.603z M36.241,36.241c-0.781,0.781-2.047,0.781-2.828,0l-7.425-7.425l-7.778,7.778 c-0.781,0.781-2.047,0.781-2.828,0c-0.781-0.781-0.781-2.047,0-2.828l7.778-7.778l-7.425-7.425c-0.781-0.781-0.781-2.048,0-2.828 c0.781-0.781,2.047-0.781,2.828,0l7.425,7.425l7.071-7.071c0.781-0.781,2.047-0.781,2.828,0c0.781,0.781,0.781,2.047,0,2.828 l-7.071,7.071l7.425,7.425C37.022,34.194,37.022,35.46,36.241,36.241z"
id="path1505"
fill="#6f6f6f" />
</g>
<g>
<g
id="g1509">
</g>
<g>
<g
id="g1511">
</g>
<g>
<g
id="g1513">
</g>
<g>
<g
id="g1515">
</g>
<g>
<g
id="g1517">
</g>
<g>
<g
id="g1519">
</g>
<g>
<g
id="g1521">
</g>
<g>
<g
id="g1523">
</g>
<g>
<g
id="g1525">
</g>
<g>
<g
id="g1527">
</g>
<g>
<g
id="g1529">
</g>
<g>
<g
id="g1531">
</g>
<g>
<g
id="g1533">
</g>
<g>
<g
id="g1535">
</g>
<g>
<g
id="g1537">
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 192 512"
version="1.1"
id="svg846"
sodipodi:docname="more.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata852">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs850" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="940"
inkscape:window-height="1027"
id="namedview848"
showgrid="false"
inkscape:zoom="1.6035156"
inkscape:cx="62.635809"
inkscape:cy="256"
inkscape:window-x="964"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg846" />
<path
d="m 96,209.92 c 25.472,0 46.08,20.608 46.08,46.08 0,25.472 -20.608,46.08 -46.08,46.08 -25.472,0 -46.08,-20.608 -46.08,-46.08 0,-25.472 20.608,-46.08 46.08,-46.08 z M 49.92,91.36 c 0,25.472 20.608,46.08 46.08,46.08 25.472,0 46.08,-20.608 46.08,-46.08 0,-25.472 -20.608,-46.08 -46.08,-46.08 -25.472,0 -46.08,20.608 -46.08,46.08 z m 0,329.28 c 0,25.472 20.608,46.08 46.08,46.08 25.472,0 46.08,-20.608 46.08,-46.08 0,-25.472 -20.608,-46.08 -46.08,-46.08 -25.472,0 -46.08,20.608 -46.08,46.08 z"
id="path844"
style="fill:#000000;fill-opacity:0.53333336;stroke-width:0.63999999"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 384 512"
version="1.1"
id="svg4623"
sodipodi:docname="schedule.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata4629">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4627" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="940"
inkscape:window-height="1027"
id="namedview4625"
showgrid="false"
inkscape:zoom="0.4609375"
inkscape:cx="-269.01695"
inkscape:cy="256"
inkscape:window-x="964"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg4623" />
<path
d="M360 0H24C10.745 0 0 10.745 0 24v16c0 13.255 10.745 24 24 24 0 90.965 51.016 167.734 120.842 192C75.016 280.266 24 357.035 24 448c-13.255 0-24 10.745-24 24v16c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24 0-90.965-51.016-167.734-120.842-192C308.984 231.734 360 154.965 360 64c13.255 0 24-10.745 24-24V24c0-13.255-10.745-24-24-24zm-64 448H88c0-77.458 46.204-144 104-144 57.786 0 104 66.517 104 144z"
id="path4621"
style="fill:#000000;fill-opacity:0.2" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 512 512"
version="1.1"
id="svg6029"
sodipodi:docname="todo_white.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata6035">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6033" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview6031"
showgrid="false"
inkscape:zoom="1.84375"
inkscape:cx="251.21851"
inkscape:cy="298.38464"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg6029" />
<path
d="m 208,128 h 288 c 8.8,0 16,-7.2 16,-16 V 80 c 0,-8.8 -7.2,-16 -16,-16 H 208 c -8.8,0 -16,7.2 -16,16 v 32 c 0,8.8 7.2,16 16,16 z m 0,160 h 288 c 8.8,0 16,-7.2 16,-16 v -32 c 0,-8.8 -7.2,-16 -16,-16 H 208 c -8.8,0 -16,7.2 -16,16 v 32 c 0,8.8 7.2,16 16,16 z m 0,160 h 288 c 8.8,0 16,-7.2 16,-16 v -32 c 0,-8.8 -7.2,-16 -16,-16 H 208 c -8.8,0 -16,7.2 -16,16 v 32 c 0,8.8 7.2,16 16,16 z M 64,368 c -26.5,0 -48.6,21.5 -48.6,48 0,26.5 22.1,48 48.6,48 26.5,0 48,-21.5 48,-48 0,-26.5 -21.5,-48 -48,-48 z M 156.5,69 84.3,141.2 68.7,156.8 c -4.7,4.7 -12.9,4.7 -17.6,0 L 3.5,109.4 c -4.7,-4.7 -4.7,-12.3 0,-17 L 19.2,76.7 c 4.7,-4.7 12.3,-4.7 17,0 l 22.7,22.1 63.7,-63.3 c 4.7,-4.7 12.3,-4.7 17,0 l 17,16.5 c 4.6,4.7 4.6,12.3 -0.1,17 z m 0,159.6 -72.2,72.2 -15.7,15.7 c -4.7,4.7 -12.9,4.7 -17.6,0 L 3.5,269 c -4.7,-4.7 -4.7,-12.3 0,-17 l 15.7,-15.7 c 4.7,-4.7 12.3,-4.7 17,0 l 22.7,22.1 63.7,-63.7 c 4.7,-4.7 12.3,-4.7 17,0 l 17,17 c 4.6,4.6 4.6,12.2 -0.1,16.9 z"
id="path6027"
style="fill:#f2f2f2;fill-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssssssssssssssssssssssssssssssscsccsscccccsscsssssccssss" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,114 @@
/*
* This component was build as an alternative to the select component. The idea is
* we replace the dirty select on desktop by something more fancy but not on ios/android
* as there's just no reason for doing that.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { Icon, NgIf } from "./";
import { debounce } from "../helpers/";
import './dropdown.scss';
export class Dropdown extends React.Component {
constructor(props){
super(props);
this.state = {
button: false
};
this.$dropdown = null;
this.closeDropdown = this.closeDropdown.bind(this);
this.toggleDropdown = this.toggleDropdown.bind(this);
}
componentDidMount(){
this.$dropdown = ReactDOM.findDOMNode(this).querySelector(".dropdown_button");
// This is not really the "react" way of doing things but we needed to use both a
// click on the button and on the body (to exit the dropdown). we had issues
// that were impossible to solve the "react" way such as the dropdown button click
// event was triggered after the body click which makes it hard to cancel it ...
this.$dropdown.addEventListener("click", this.toggleDropdown);
}
componentWillUnmount(){
this.$dropdown.removeEventListener("click", this.toggleDropdown);
document.body.removeEventListener("click", this.closeDropdown);
}
onSelect(name){
this.props.onChange(name);
}
closeDropdown(){
document.body.removeEventListener("click", this.closeDropdown);
this.setState({button: false});
}
toggleDropdown(e){
document.body.removeEventListener("click", this.closeDropdown);
this.setState({button: !this.state.button}, () => {
if(this.state.button === true){
requestAnimationFrame(() => {
document.body.addEventListener("click", this.closeDropdown);
});
}
});
}
render(){
const button = this.props.children[0];
if(button.type.name !== 'DropdownButton') throw("First children should be of type DropdownButton");
const dropdown = React.cloneElement(this.props.children[1], {onSelect: this.onSelect.bind(this)});
if(dropdown.type.name !== 'DropdownList') throw("Second children should be of type DropdownList");
return (
<div className={"component_dropdown"+(this.state.button ? " active" : "")}>
{ button }
{ dropdown }
</div>
);
}
}
export const DropdownButton = (props) => {
return (
<div className="dropdown_button">
{ props.children }
</div>
);
}
export const DropdownList = (props) => {
const childrens = Array.isArray(props.children) ? props.children : [props.children];
return (
<div className="dropdown_container">
<ul>
{
childrens.map((children, index) => {
const child = React.cloneElement(children, {onSelect: props.onSelect });
return (
<li key={index}>{child}</li>
);
})
}
</ul>
</div>
);
};
export const DropdownItem = (props) => {
return (
<div onClick={props.onSelect.bind(null, props.name)}>
{props.children}
<NgIf cond={!!props.icon} type="inline">
<span style={{float: 'right'}}>
<Icon name={props.icon} />
</span>
</NgIf>
</div>
);
};

View File

@ -0,0 +1,49 @@
.component_dropdown{
position: relative;
.dropdown_container{display: none; position: absolute; right: 0;}
.dropdown_button{
border: 1px solid white;
border-radius: 4px;
padding: 5px;
min-width: 20px;
text-align: center;
}
.dropdown_container{
padding-top: 3px;
z-index: 2;
ul{
margin: 0;
list-style-type: none;
background: white;
border: 1px solid var(--bg-color);
box-shadow: 1px 1px 2px rgba(0,0,0,0.1);
color: var(--emphasis-secondary);
border-radius: 3px;
padding: 3px 0px;
font-size: 0.92em;
li{
display: flex;
> div{
width: 150px;
padding: 8px 5px 8px 10px;
}
}
}
}
}
.component_dropdown{
&.active{
.dropdown_container{
display: block;
li:hover{
background: var(--bg-color);
}
}
.dropdown_button{
border-color: var(--bg-color);
box-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
}
}

View File

@ -11,11 +11,21 @@ import img_delete from "../assets/img/delete.svg";
import img_bucket from "../assets/img/bucket.svg";
import img_link from "../assets/img/link.svg";
import img_loading from "../assets/img/loader.svg";
import img_download from "../assets/img/download.svg";
import img_play from "../assets/img/play.svg";
import img_pause from "../assets/img/pause.svg";
import img_error from "../assets/img/error.svg";
import img_loading_white from "../assets/img/loader_white.svg";
import img_download_white from "../assets/img/download_white.svg";
import img_todo_white from '../assets/img/todo_white.svg';
import img_calendar_white from '../assets/img/calendar_white.svg';
import img_arrow_right from '../assets/img/arrow_right.svg';
import img_more from '../assets/img/more.svg';
import img_close from '../assets/img/close.svg';
import img_schedule from '../assets/img/schedule.svg';
import img_deadline from '../assets/img/deadline.svg';
import img_arrow_down from '../assets/img/arrow-down.svg';
import img_arrow_up_double from '../assets/img/arrow-up-double.svg';
import img_arrow_down_double from '../assets/img/arrow-down-double.svg';
export const Icon = (props) => {
let img;
@ -39,8 +49,8 @@ export const Icon = (props) => {
img = img_link;
}else if(props.name === 'loading'){
img = img_loader;
}else if(props.name === 'download'){
img = img_download;
}else if(props.name === 'download_white'){
img = img_download_white;
}else if(props.name === 'play'){
img = img_play;
}else if(props.name === 'pause'){
@ -49,6 +59,26 @@ export const Icon = (props) => {
img = img_error;
}else if(props.name === 'loading_white'){
img = img_loading_white;
}else if(props.name === 'calendar_white'){
img = img_calendar_white;
}else if(props.name === 'schedule'){
img = img_schedule;
}else if(props.name === 'deadline'){
img = img_deadline;
}else if(props.name === 'todo_white'){
img = img_todo_white;
}else if(props.name === 'arrow_right'){
img = img_arrow_right;
}else if(props.name === 'more'){
img = img_more;
}else if(props.name === 'close'){
img = img_close;
}else if(props.name === 'arrow_up_double'){
img = img_arrow_up_double;
}else if(props.name === 'arrow_down_double'){
img = img_arrow_down_double;
}else if(props.name === 'arrow_down'){
img = img_arrow_down;
}else{
throw('unknown icon');
}
@ -60,4 +90,4 @@ export const Icon = (props) => {
src={img}
alt={props.name}/>
);
}
};

View File

@ -17,6 +17,7 @@ export { Notification } from './notification';
export { Alert } from './alert';
export { Audio } from './audio';
export { Video } from './video';
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
//export { Connect } from './connect';
// Those are commented because they will delivered as a separate chunk
// export { Editor } from './editor';

View File

@ -3,14 +3,17 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import PropTypes from 'prop-types';
import { Input, Button, NgIf } from './';
import { debounce } from '../helpers/';
import './modal.scss';
export class Modal extends React.Component {
constructor(props){
super(props);
this.state = {
marginTop: this._marginTop()
marginTop: -1
};
this._resetMargin = debounce(this._resetMargin.bind(this), 100);
this._onEscapeKeyPress = this._onEscapeKeyPress.bind(this);
}
onClick(e){
@ -20,11 +23,25 @@ export class Modal extends React.Component {
}
componentDidMount(){
this._resetMargin();
window.addEventListener("resize", this._resetMargin);
window.addEventListener('keydown', this._onEscapeKeyPress);
}
componentWillUnmount(){
window.removeEventListener("resize", this._resetMargin);
window.removeEventListener('keydown', this._onEscapeKeyPress);
}
_resetMargin(){
this.setState({marginTop: this._marginTop()});
}
componentWillUnmount(){
_onEscapeKeyPress(e){
if(e.keyCode === 27){
this.props.onQuit && this.props.onQuit();
}
}
_marginTop(){
let size = 300;
@ -45,8 +62,8 @@ export class Modal extends React.Component {
transitionAppear={true} transitionAppearTimeout={300}
>
<NgIf key={"modal-"+this.props.isActive} cond={this.props.isActive}>
<div className="component_modal" onClick={this.onClick.bind(this)} id="modal-box">
<div key="random" style={{margin: this.state.marginTop+'px auto 0 auto'}}>
<div className={"component_modal"+(this.props.className? " " + this.props.className : "")} onClick={this.onClick.bind(this)} id="modal-box">
<div style={{margin: this.state.marginTop+'px auto 0 auto', visibility: this.state.marginTop === -1 ? "hidden" : "visible"}}>
{this.props.children}
</div>
</div>

View File

@ -57,24 +57,24 @@
.modal-appear > div, .modal-enter > div{ opacity: 0;}
.modal-appear.modal-appear-active > div, .modal-enter.modal-enter-active > div{
opacity: 1;
transition: opacity 0.2s ease;
transition: opacity 0.3s ease;
}
// box
.modal-appear > div > div, .modal-enter > div > div{
opacity: 0;
transform-origin: top center;
transform: translateY(10px);
}
.modal-appear.modal-appear-active > div > div, .modal-enter.modal-enter-active > div > div{
opacity: 1;
transform: translateY(0);
transition: all 0.2s ease-out;
transition-duration: 0.2s;
transition-timing-function: ease-out;
transition-property: opacity, transform;
transition-delay: 0.1s;
}
/***********************/
/* LEAVING TRANSITION */
// background
@ -85,8 +85,9 @@
transition-delay: 0.1s;
}
// box
.modal-leave > div > div { opacity: 1; }
.modal-leave > div > div { transform: translateY(0); opacity: 1;}
.modal-leave.modal-leave-active > div > div{
transform: translateY(20px);
opacity: 0;
transition: all 0.3s ease;
transition: all 0.2s ease-out;
}

View File

@ -13,7 +13,6 @@ export class ModalPrompt extends React.Component {
};
this.onCancel = this.onCancel.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onEscapeKeyPress = this.onEscapeKeyPress.bind(this);
}
componentDidMount(){
@ -27,16 +26,12 @@ export class ModalPrompt extends React.Component {
fns: {ok: okCallback, cancel: cancelCallback}
});
});
window.addEventListener('keydown', this.onEscapeKeyPress);
}
componentDidUmount(){
window.removeEventListener('keydown', this.onEscapeKeyPress);
}
onCancel(){
this.setState({appear: false});
this.state.fns.cancel();
this.state.fns && this.state.fns.cancel();
}
onSubmit(e){

View File

@ -11,3 +11,4 @@ export { invalidate, http_get, http_post, http_delete } from './ajax';
export { screenHeight, screenHeightWithMenubar } from './dom';
export { prompt } from './prompt';
export { notify } from './notify';
export { guid } from './random';

220
client/helpers/org.js Normal file
View File

@ -0,0 +1,220 @@
import { guid } from "./";
export function extractTodos(text){
const headlines = parse(text);
let todos = [];
for(let i=0; i < headlines.length; i++){
let todo = formatTodo(headlines[i]);
if(todo.status){
todos.push(todo);
}
}
return todos
.sort((a,b) => {
if(a.status === "DONE") return +1;
else if(a.todo_status === "todo") return -1;
return 0;
});
function formatTodo(thing){
return {
key: thing.header.todo_keyword,
id: thing.id,
line: thing.header.line,
title: thing.header.title,
status: thing.header.todo_keyword,
todo_status: ["TODO", "NEXT", "DOING", "WAITING"].indexOf(thing.header.todo_keyword) !== -1 ? 'todo' : 'done',
is_overdue: _is_overdue(thing.header.todo_keyword, thing.timestamps),
tasks: thing.subtasks,
tags: thing.header.tags
};
}
}
export function extractEvents(text){
const headlines = parse(text);
let events = [];
for(let i=0; i < headlines.length; i++){
events = events.concat(
formatEvents(headlines[i])
);
}
return events.sort((a, b) => a.date - b.date);
function formatEvents(thing){
let events = [];
for(let i=0; i < thing.timestamps.length; i++){
let timestamp = thing.timestamps[i];
if(timestamp.active === false) continue;
let event = {
id: thing.id,
line: thing.header.line,
title: thing.header.title,
status: thing.header.todo_keyword,
todo_status: function(keyword){
if(!keyword) return null;
return ["TODO", "NEXT", "DOING", "WAITING"].indexOf(keyword) !== -1 ? 'todo' : 'done';
}(thing.header.todo_keyword),
is_overdue: _is_overdue(thing.header.todo_keyword, thing.timestamps),
tasks: [],
tags: thing.header.tags
};
if(event.todo_status === 'done') continue;
event.date = timestamp.timestamp;
const today = new Date();
today.setHours(23);
today.setMinutes(59);
today.setSeconds(59);
today.setMilliseconds(999);
if(event.date < today){
event.date = today;
}
event.key = Intl.DateTimeFormat().format(event.date);
if(timestamp.repeat){
if(timestamp.repeat.interval === "m"){
events.push(event);
}else{
if(timestamp.repeat.interval === "y"){
timestamp.repeat.n *= 365;
}else if(timestamp.repeat.interval === "w"){
timestamp.repeat.n *= 7;
}
const n_days = timestamp.repeat.n;
let today = normalise(new Date());
for(let j=0;j<30;j++){
if(((today - normalise(timestamp.timestamp)) / 1000*60*60*24) % n_days === 0){
event.date = today.getTime();
event.key = Intl.DateTimeFormat().format(today);
events.push(JSON.parse(JSON.stringify((event))));
}
today.setDate(today.getDate() + 1);
}
}
}else{
events.push(event);
}
}
return events;
function normalise(date){
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
}
}
}
export function parse(content){
let todos = [], todo = reset(), data, text;
const lines = content.split("\n");
for(let i = 0; i<lines.length; i++){
text = lines[i];
if(data = parse_header(text, i)){
if(todo.header){
todos.push(todo);
todo = reset();
}
todo.header = data;
}else if(data = parse_timestamp(text, i)){
todo.timestamps.push(data);
}else if(data = parse_subtask(text, i)){
todo.subtasks.push(data);
}
if(i === lines.length - 1 && todo.header){
todos.push(todo);
}
}
return todos;
function reset(){
return {id: guid(), timestamps: [], subtasks: []};
}
}
function parse_header(text, line){
const match = text.match(/^(\*+)\s(?:([A-Z]{3,})\s){0,1}(?:\[\#([A-C])\]\s){0,1}(.*?)(?:\s+\:((?:[a-z]+\:){1,})){0,1}$/);
if(!match) return null;
return {
line: line,
level: RegExp.$1.length,
todo_keyword: RegExp.$2 || null,
priority: RegExp.$3 || null,
title: RegExp.$4 || "Empty Heading",
tags: RegExp.$5
.replace(/:/g, " ")
.trim()
.split(" ")
.filter((e) => e)
};
}
function parse_subtask(text, line){
const match = text.match(/(?:-|\d+[\.\)])\s\[([X\s-])\]\s(.*)/);
if(!match) return null;
return {
line: line,
status: function(state){
if(state === "X") return "DONE";
else if(state === " ") return "TODO";
return null;
}(match[1]),
title: match[2] || "Empty task",
}
}
function parse_timestamp(text, line){
const match = text.match(/(?:([A-Z]+)\:\s){0,1}([<\[])(\d{4}-\d{2}-\d{2})[^>](?:[A-Z][a-z]{2})(?:\s([0-9]{2}\:[0-9]{2})){0,1}(?:\-([0-9]{2}\:[0-9]{2})){0,1}(?:\s(\+{1,2}[0-9]+[dwmy])){0,1}[\>\]](?:--[<\[](\d{4}-\d{2}-\d{2})\s[A-Z][a-z]{2}\s(\d{2}:\d{2}){0,1}[>\]]){0,1}/);
if(!match) return null;
// https://orgmode.org/manual/Timestamps.html
return {
line: line,
keyword: match[1],
active: match[2] === "<" ? true : false,
timestamp: new Date(match[3] + (match[4] ? " "+match[4] : "")),
range: function(start_date, start_time = "", start_time_end, end_date = "", end_time = ""){
if(start_time_end && !end_date){
return new Date(start_date+" "+start_time_end) - new Date(start_date+" "+start_time);
}
if(end_date){
return new Date(end_date+" "+end_time) - new Date(start_date+" "+start_time);
}
return null;
}(match[3], match[4], match[5], match[7], match[8]),
repeat: function(keyword){
if(!keyword) return;
return {
n: parseInt(keyword.replace(/^.*([0-9]+).*$/, "$1")),
interval: keyword.replace(/^.*([dwmy])$/, "$1")
};
}(match[6])
};
}
function _is_overdue(status, timestamp){
if(status !== "TODO") return false;
return timestamp.filter((timeObj) => {
if(new Date() < timeObj.date) return false;
if(timeObj.keyword === "DEADLINE" || timeObj.keyword === "SCHEDULE") return true;
return false;
}).length > 0 ? true : false;
}
function _date_label(date){
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return window.Intl.DateTimeFormat().format(date);
}

8
client/helpers/random.js Normal file
View File

@ -0,0 +1,8 @@
export function guid(){
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

View File

@ -21,6 +21,7 @@ import 'codemirror/addon/fold/foldgutter.css';
import './editor.scss';
import { debounce, screenHeightWithMenubar } from '../../helpers/';
import { org_shifttab } from './editor/emacs-org';
import config from '../../../config_client';
export class Editor extends React.Component {
@ -32,7 +33,7 @@ export class Editor extends React.Component {
filename: this.props.filename,
height: 0
};
this.resetHeight = debounce(this.resetHeight.bind(this), 100);
this.resetHeight = debounce(this.resetHeight.bind(this), 0);
}
componentDidMount(){
@ -41,12 +42,31 @@ export class Editor extends React.Component {
if(this.state.loading === null) this.setState({loading: true});
}, 200);
});
this.loadMode(this.props.filename)
.then((res) => new Promise((done) => this.setState({loading: false}, () => done(res))))
.then(loadCodeMirror.bind(this));
.then(loadCodeMirror.bind(this))
.then(() => {
this.props.event.subscribe((data) => {
const [type, value] = data;
if(type === "goTo"){
const pY = this.state.editor.charCoords({line: value - 1, ch: 0}, "local").top;
this.state.editor.scrollTo(null, pY);
//this.state.editor.setCursor({line: new_props.currentLine, ch: 2});
}else if(type === "refresh"){
this.state.editor.setValue(this.props.content);
}else if(type === "fold"){
this.props.onFoldChange(
org_shifttab(this.state.editor)
);
}
});
});
function loadCodeMirror(data){
const [CodeMirror, mode] = data;
const size_small = 500;
let editor = CodeMirror(document.getElementById('editor'), {
value: this.props.content,
@ -59,11 +79,19 @@ export class Editor extends React.Component {
}
});
//debugger;
if(!('ontouchstart' in window)) editor.focus();
if(CodeMirror.afterInit){
CodeMirror.afterInit(editor);
CodeMirror.afterInit(editor, (key, value) => {
if(key === "shifttab"){
this.props.onFoldChange(value);
}
});
}
this.setState({editor: editor});
this.props.onModeChange(mode);
editor.on('change', (edit) => {
if(this.props.onChange){
@ -71,13 +99,9 @@ export class Editor extends React.Component {
}
});
if(config.god_editor_mode === true){
editor.addKeyMap({
"Ctrl-X Ctrl-C": function(cm){
history.back();
}
});
}
CodeMirror.commands.quit = () => {
window.history.back();
};
CodeMirror.commands.save = () => {
this.props.onSave && this.props.onSave();

View File

@ -7,6 +7,14 @@
}
.CodeMirror-scroll { -webkit-overflow-scrolling: touch; }
.CodeMirror-foldmarker{
visibility: hidden;
&:before{
content: "...";
visibility: visible;
}
}
/* SEARCH */
.CodeMirror-dialog {
position: fixed;
@ -42,7 +50,7 @@
}
/* Highlight Theme */
.cm-s-default .cm-header {color: #3E7AA6; font-size: 1.15em;}
.cm-s-default .cm-header {color: #3E7AA6; font-size: 1.15em; line-height: 1em;}
.cm-s-default .cm-keyword {color: #3E7AA6;}
.cm-s-default .cm-header.cm-org-level-star{color: #6f6f6f;}
.cm-s-default .cm-header.cm-org-todo{color: #FF8355;}

View File

@ -0,0 +1,594 @@
export const org_cycle = (cm) => {
let pos = cm.getCursor();
isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos);
};
let state = {
stab: 'CONTENT'
};
export const org_set_default_fold = (cm) => {
set_folding_mode(cm, state.stab);
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);
}
function folding_mode_overview(cm){
cm.operation(function() {
for (var 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 (var 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 (var 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;
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;
let p = null, 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,
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)),
line = cm.getCursor().line,
content = cm.getLine(line),
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]]),
line = cm.getCursor().line,
content = cm.getLine(line),
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,
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)){
let 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)){
let 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)){
}
});
}
/*
* 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)){
let a = isItemList(cm, p.end + 1);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
}
}else if(p = isNumberedList(cm, line)){
let 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)){
}
});
}
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 content, tmp, 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,
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,
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){
const content = cm.getLine(line);
if(/^\*+\s/.test(content) === false) return null;
const match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/);
if(match === null) return null;
return {
type: 'title',
level: match[1].length,
content: match[3],
start: line,
end: 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, 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, 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, p, match, 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, p, match, 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}),
to_content = cm.getRange({line: to[0], ch: 0}, {line: to[1], ch: cm.getLine(to[1]).length}),
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
});
}
}
function isEmpty(content){
return content.trim() === "";
}
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;
}

View File

@ -1,10 +1,15 @@
import 'codemirror/addon/mode/simple';
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_default_fold
} from './emacs-org';
CodeMirror.__mode = 'orgmode';
CodeMirror.defineSimpleMode("orgmode", {
start: [
{regex: /^(^\*{1,}\s)(TODO|DOING|WAITING|NEXT){0,1}(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED){0,1}(.*)$/, token: ["header org-level-star", "header org-todo", "header org-done", "header"]},
{regex: /^(\*{1,}\s)(TODO|DOING|WAITING|NEXT|)(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(.*?)(\:[\S]+\:|)$/, token: ["header org-level-star","header org-todo","header org-done","header", "comment"]},
{regex: /(\+[^\+]+\+)/, token: ["strikethrough"]},
{regex: /(\*[^\*]+\*)/, token: ["strong"]},
{regex: /(\/[^\/]+\/)/, token: ["em"]},
@ -16,7 +21,7 @@ CodeMirror.defineSimpleMode("orgmode", {
{regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments
{regex: /:?[A-Z_]+\:.*/, token: "comment"}, // property drawers
{regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments
{regex: /(CLOCK\:|SHEDULED\:)(\s.+)/, token: ["comment", "keyword"]}
{regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"]}
],
env: [
{regex: /.*?\#\+END_[A-Z]*/, token: "comment", next: "start"},
@ -90,216 +95,35 @@ CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
}
});
CodeMirror.afterInit = function(editor){
let state = {
stab: 'SHOW_ALL'
};
CodeMirror.afterInit = function(editor, fn){
editor.setOption("extraKeys", {
"Tab": function(cm) {
let pos = cm.getCursor();
isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos);
},
"Shift-Tab": function(cm){
if(state.stab === "SHOW_ALL"){
state.stab = 'OVERVIEW';
cm.operation(function() {
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){
fold(cm, CodeMirror.Pos(i, 0));
}
"Tab": function(cm) { org_cycle(cm); },
"Shift-Tab": function(cm){ fn('shifttab', org_shifttab(cm)); },
"Alt-Left": function(cm){ org_metaleft(cm); },
"Alt-Right": function(cm){ org_metaright(cm); },
"Alt-Enter": function(cm){ org_meta_return(cm); },
"Alt-Up": function(cm){ org_metaup(cm); },
"Alt-Down": function(cm){ org_metadown(cm); },
"Shift-Alt-Enter": function(cm){ org_insert_todo_heading(cm); },
"Shift-Left": function(cm){ org_shiftleft(cm); },
"Shift-Right": function(cm){ org_shiftright(cm); },
});
}else if(state.stab === "OVERVIEW"){
state.stab = 'CONTENT';
cm.operation(function() {
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){
if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){
if(/^\* /.test(cm.getLine(i))){
unfold(cm, CodeMirror.Pos(i, 0));
}
}
}
});
}else if(state.stab === "CONTENT"){
state.stab = 'SHOW_ALL';
cm.operation(function() {
for (var 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));
}
}
});
}
},
"Alt-Left": function(cm){
const line = cm.getCursor().line,
content = cm.getLine(line);
let p = null;
fn('shifttab', org_set_default_fold(editor));
if(p = isTitle(content)){
if(p['level'] > 1) cm.replaceRange('', {line: line, ch: 0}, {line: line, ch: 1});
}else if(p = isItemList(content)){
if(p['level'] > 0) cm.replaceRange('', {line: line, ch: 0}, {line: line, ch: 2});
}else if(isNumberedList(content)){
}
},
"Alt-Right": function(cm){
const line = cm.getCursor().line,
content = cm.getLine(line);
let p = null;
if(p = isItemList(content)){
cm.replaceRange(' ', {line: line, ch: 0});
}else if(p = isNumberedList(content)){
}else if(p = isTitle(content)){
cm.replaceRange('*', {line: line, ch: 0});
}
},
"Alt-Enter": function(cm){
const line = cm.getCursor().line,
content = cm.getLine(line);
let p = null;
if(p = isItemList(content)){
const level = p.level;
cm.replaceRange('\n'+" ".repeat(level*2)+'- ', {line: line, ch: content.length});
cm.setCursor({line: line+1, ch: 2+level*2});
}else if(p = isNumberedList(content)){
}else if(p = isTitle(content)){
}
},
"Alt-Up": function(cm){
const line = cm.getCursor().line,
content = cm.getLine(line);
let p = null;
if(p = isItemList(content)){
}else if(p = isNumberedList(content)){
}else if(p = isTitle(content)){
}
},
"Alt-Down": function(cm){
const line = cm.getCursor().line,
content = cm.getLine(line);
let p = null;
if(p = isItemList(content)){
}else if(p = isNumberedList(content)){
}else if(p = isTitle(content)){
}
},
"Shift-Alt-Enter": function(cm){
const line = cm.getCursor().line,
content = cm.getLine(line);
let p = null;
if(p = isItemList(content)){
}else if(p = isNumberedList(content)){
}else if(p = isTitle(content)){
}
},
"Shift-Left": function(cm){
const cycles = ["TODO", "", "DONE", "TODO"],
line = cm.getCursor().line,
content = cm.getLine(line),
params = isTitle(content);
if(params === null) return;
if(cycles.indexOf(params['status']) === -1) params['status'] = "";
params['status'] = cycles[cycles.indexOf(params['status']) + 1];
cm.replaceRange(makeTitle(params), {line: line, ch: 0}, {line: line, ch: content.length});
},
"Shift-Right": function(cm){
const cycles = ["TODO", "DONE", "", "TODO"],
line = cm.getCursor().line,
content = cm.getLine(line),
params = isTitle(content);
if(params === null) return;
if(cycles.indexOf(params['status']) === -1) params['status'] = "TODO";
params['status'] = cycles[cycles.indexOf(params['status']) + 1];
cm.replaceRange(makeTitle(params), {line: line, ch: 0}, {line: line, ch: content.length});
editor.addKeyMap({
"Ctrl-X Ctrl-C": function(cm){
cm.execCommand('quit');
}
});
function makeTitle(p){
if(p['status'] === ""){
return "*".repeat(p['level'])+" "+p['content'];
}
return "*".repeat(p['level'])+" "+p['status']+" "+p['content'];
}
function makeNumberedList(p){
}
function makeItemList(p){
}
function previousOfType(cm, line){
let content, tmp, i;
for(i=line - 1, k=0; i>0; i--){
content = cm.getLine(line);
tmp = isItemList(content);
if(tmp !== null) return ['list', tmp];
tmp = isNumberedList(content);
if(tmp !== null) return ['numbered', tmp];
tmp = isTitle(content);
if(tmp !== null) return ['title', tmp];
}
return null;
}
function isItemList(content){
if(content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ") return null;
const padding = content.replace(/^(\s*).*$/, "$1").length;
if(padding % 2 !== 0) return null;
return {
level: padding / 2,
content: content.trimLeft().replace(/^\s*\-\s(.*)$/, '$1')
};
}
function isNumberedList(content){
if(/^[0-9]+[\.\)]\s.*$/.test(content.trimLeft()) === false) return null;
const padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length;
if(padding % 2 !== 0) return null;
return {
level:padding / 2,
n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")),
content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, '$1'),
separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, '$1')
};
}
function isTitle(content){
if(/^\*+\s/.test(content) === false) return null;
const match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/);
if(match === null) return null;
return {
level: match[1].length,
status: match[2].trim(),
content: match[3]
};
}
function isEmpty(content){
return content.trim() === "";
}
function fold(cm, start){
cm.foldCode(start, null, "fold");
}
function unfold(cm, start){
cm.foldCode(start, null, "unfold");
}
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;
}
editor.on('touchstart', function(cm, e){
setTimeout(() => {
isFold(cm, cm.getCursor()) ? unfold(cm, cm.getCursor()) : fold(cm, cm.getCursor())
}, 150);
});
// fold everything except headers by default
editor.operation(function() {
for (var i = 0; i < editor.lineCount() ; i++) {
@ -308,17 +132,8 @@ CodeMirror.afterInit = function(editor){
}
}
});
function collapseWidget(){
let $widget = document.createElement('span');
$widget.appendChild(document.createTextNode('colapse'));
return $widget;
}
function expandWidget(){
let $widget = document.createElement('span');
$widget.appendChild(document.createTextNode('expand'));
return $widget;
}
}
export default CodeMirror;

View File

@ -1,9 +1,12 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { Subject } from 'rxjs/Subject';
import { NgIf, Fab, Icon } from '../../components/';
import { Editor } from './editor';
import { MenuBar } from './menubar';
import { OrgTodosViewer, OrgEventsViewer } from './org_viewer';
import './ide.scss';
@ -11,20 +14,16 @@ export class IDE extends React.Component {
constructor(props){
super(props);
this.state = {
event: new Subject(),
contentToSave: props.content,
needSaving: false
needSaving: false,
appear_agenda: false,
appear_todo: false,
mode: null,
folding: null
};
}
onContentUpdate(text){
if(this.props.content === text){
this.props.needSavingUpdate(false);
}else{
this.props.needSavingUpdate(true);
}
this.setState({contentToSave: text})
}
save(){
if(this.props.needSaving === false) return;
@ -37,14 +36,69 @@ export class IDE extends React.Component {
file = blob;
}
this.props.onSave(file)
.then(() => this.props.needSavingUpdate(false))
.then(() => this.props.needSavingUpdate(false));
}
onUpdate(property, refresh, value){
this.setState({ [property]: value }, () => {
if(refresh){
this.state.event.next(["refresh"]);
}
if(this.props.content === this.state.contentToSave){
this.props.needSavingUpdate(false);
}else{
this.props.needSavingUpdate(true);
}
});
}
/* Org Viewer specific stuff */
toggleAgenda(){
this.setState({appear_agenda: !this.state.appear_agenda});
}
toggleTodo(){
this.setState({appear_todo: !this.state.appear_todo});
}
onModeChange(){
this.state.event.next(["fold"]);
}
goTo(lineNumber){
this.state.event.next(["goTo", lineNumber]);
}
render(){
return (
<div style={{height: '100%'}}>
<MenuBar title={this.props.filename} download={this.props.url} />
<Editor onSave={this.save.bind(this)} filename={this.props.filename} content={this.props.content} onChange={this.onContentUpdate.bind(this)} />
<div style={{height: '100%'}} className="no-select">
<MenuBar title={this.props.filename} download={this.props.url}>
<NgIf type="inline" cond={this.state.mode === 'orgmode'}>
<span onClick={this.onModeChange.bind(this)}>
<NgIf cond={this.state.folding === "SHOW_ALL"} type="inline">
<Icon name="arrow_up_double"/>
</NgIf>
<NgIf cond={this.state.folding === "OVERVIEW"} type="inline">
<Icon name="arrow_down"/>
</NgIf>
<NgIf cond={this.state.folding === "CONTENT"} type="inline">
<Icon name="arrow_down_double"/>
</NgIf>
</span>
<span onClick={this.toggleAgenda.bind(this)}>
<Icon name="calendar_white"/>
</span>
<span onClick={this.toggleTodo.bind(this)}>
<Icon name="todo_white"/>
</span>
</NgIf>
</MenuBar>
<Editor onSave={this.save.bind(this)} filename={this.props.filename}
content={this.state.contentToSave}
event={this.state.event.asObservable()}
onModeChange={this.onUpdate.bind(this, 'mode', false)}
onFoldChange={this.onUpdate.bind(this, 'folding', false)}
onChange={this.onUpdate.bind(this, 'contentToSave', false)} />
<ReactCSSTransitionGroup transitionName="fab" transitionLeave={true} transitionEnter={true} transitionAppear={true} transitionAppearTimeout={400} transitionEnterTimeout={400} transitionLeaveTimeout={200}>
<NgIf key={this.props.needSaving} cond={this.props.needSaving}>
@ -52,10 +106,17 @@ export class IDE extends React.Component {
<Fab onClick={this.save.bind(this)}><Icon name="save" style={{height: '100%', width: '100%'}}/></Fab>
</NgIf>
<NgIf cond={this.props.isSaving}>
<Fab><Icon name="loading_white" style={{height: '100%', width: '100%'}}/></Fab>
<Fab><Icon name="loading" style={{height: '100%', width: '100%'}}/></Fab>
</NgIf>
</NgIf>
</ReactCSSTransitionGroup>
<OrgEventsViewer isActive={this.state.appear_agenda} content={this.state.contentToSave}
onUpdate={this.onUpdate.bind(this, "contentToSave", true)} goTo={this.goTo.bind(this)}
onQuit={this.toggleAgenda.bind(this)} />
<OrgTodosViewer isActive={this.state.appear_todo} content={this.state.contentToSave}
onUpdate={this.onUpdate.bind(this, "contentToSave", true)} goTo={this.goTo.bind(this)}
onQuit={this.toggleTodo.bind(this)} />
</div>
);
}

View File

@ -11,7 +11,12 @@ export const MenuBar = (props) => {
<div className="component_menubar">
<Container>
<ReactCSSTransitionGroup transitionName="menubar" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={350}>
<div className="action-item">
<span className="specific">
{props.children}
</span>
<DownloadButton link={props.download} name={props.title} />
</div>
<span style={{letterSpacing: '0.3px'}}>{props.title}</span>
</ReactCSSTransitionGroup>
</Container>
@ -52,16 +57,16 @@ class DownloadButton extends React.Component {
render(){
return (
<div style={{float: 'right', height: '1em'}}>
<NgIf cond={!this.state.loading} style={{display: 'inline'}}>
<span>
<NgIf cond={!this.state.loading} type="inline">
<a href={this.props.link} download={this.props.name} onClick={this.onDownloadRequest.bind(this)}>
<Icon name="download" style={{width: '15px', height: '15px'}} />
<Icon name="download_white" />
</a>
</NgIf>
<NgIf cond={this.state.loading} style={{display: 'inline'}}>
<Icon name="loading" style={{width: '15px', height: '15px'}} />
<NgIf cond={this.state.loading} type="inline">
<Icon name="loading_white" />
</NgIf>
</div>
</span>
);
}
}

View File

@ -11,6 +11,20 @@
color: var(--bg-color);
font-size: 0.9em;
}
.action-item{
float: right;
.specific{ padding-right: 10px; }
.component_icon{
height: 17px;
width: 17px;
margin: 0px 5px;
cursor: pointer;
}
span:last-child .component_icon{
margin-right: 2px;
}
}
}

View File

@ -0,0 +1,252 @@
import React from 'react';
import { StickyContainer, Sticky } from 'react-sticky';
import { Modal, Container, NgIf, Icon, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
import { extractEvents, extractTodos } from '../../helpers/org';
import './org_viewer.scss';
export const OrgEventsViewer = (props) => {
if(props.isActive !== true) return null;
const headlines = extractEvents(props.content);
return (
<OrgViewer title="Agenda" headlines={headlines} content={props.content} isActive={props.isActive}
onQuit={props.onQuit} goTo={props.goTo} onUpdate={props.onUpdate} />
);
};
export const OrgTodosViewer = (props) => {
if(props.isActive !== true) return null;
const headlines = extractTodos(props.content);
return (
<OrgViewer title="Todos" headlines={headlines} content={props.content} isActive={props.isActive}
onQuit={props.onQuit} goTo={props.goTo} onUpdate={props.onUpdate} />
);
};
class OrgViewer extends React.Component {
constructor(props){
super(props);
this.state = {
headlines: this.buildHeadlines(props.headlines)
};
}
componentWillReceiveProps(props){
this.setState({
headlines: this.buildHeadlines(props.headlines)
});
}
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){
this.state.headlines[Object.keys(this.state.headlines)[i]][j].status = state;
this.setState({
headlines: this.state.headlines
});
}
navigate(line){
this.props.onQuit();
this.props.goTo(line);
}
onTaskUpdate(type, line, value){
const content = this.props.content.split("\n");
switch(type){
case "status":
const [status, state] = value;
let next = "DONE";
if(status === "DONE") next = "TODO";
content[line] = content[line].replace(status, next);
break;
case "subtask":
if(value === "DONE"){
content[line] = content[line].replace(/\[.\]/, '[X]');
}else{
content[line] = content[line].replace(/\[.\]/, '[ ]');
}
break;
case "schedule":
break;
case "deadline":
break;
};
this.props.onUpdate(content.join("\n"));
}
render(){
return (
<Modal className="todo-modal" isActive={this.props.isActive} onQuit={this.props.onQuit}>
<div className="modal-top no-select">
<span onClick={this.props.onQuit}>
<Icon name="close"/>
</span>
<h1>{this.props.title}</h1>
</div>
<NgIf cond={this.props.headlines.length === 0} className="nothing">
<div className="big">Nothing</div>
<div className="contrib">
If you got some free time, help us making Nuage better by <a href="https://github.com/mickael-kerjean/nuage">contributing</a>? ;)
</div>
</NgIf>
<NgIf cond={this.props.headlines.length > 0}>
<StickyContainer className="container" style={{height: window.innerHeight > 650 ? 500 : window.innerHeight - 150, overflowY: "auto", padding: "0 10px"}}>
{
Object.keys(this.state.headlines).map((list, i) => {
return (
<div key={i}>
<Sticky relative>
{
({isSticky, wasSticky, style, distanceFromTop, distanceFromBottom, calculatedHeight}) => {
return (
<div className="sticky_header no-select" style={{...style, overflow: "auto", background: "white"}}>
<h2>{list} <span>{this.state.headlines[list].length}</span></h2>
</div>
);
}
}
</Sticky>
<div className="list">
{
this.state.headlines[list].map((headline, j) => {
return (
<Headline
type={this.props.title.toLowerCase()}
onTaskUpdate={this.onTaskUpdate.bind(this)}
onChange={this.onChange.bind(this, i, j)}
tasks={headline.tasks}
key={j} title={headline.title}
tags={headline.tags}
line={headline.line}
status={headline.status || null}
todo_status={headline.todo_status}
goTo={this.navigate.bind(this, headline.line)} />
);
})
}
</div>
</div>
);
})
}
</StickyContainer>
</NgIf>
</Modal>
);
}
}
class Headline extends React.Component {
constructor(props){
super(props);
this.state = {
properties: false
};
}
onMenuAction(key, value){
if(key === "navigate"){
this.props.goTo();
}else if(key === "properties"){
this.setState({properties: !this.state.properties});
}
}
render(){
return (
<div className="component_headline">
<div className={"no-select headline-main "+this.props.todo_status + " " +(this.props.is_overdue === true ? "overdue" : "")}>
<div className="title" onClick={this.props.onTaskUpdate.bind(this, 'status', this.props.line, [this.props.status, this.props.todo_status])}>
<div>
<span>{this.props.title}</span>
<div className="tags">
{
this.props.tags.map((tag, i) => {
return (
<span className="tag" key={i}>{tag}</span>
);
})
}
</div>
</div>
</div>
<Dropdown onChange={this.onMenuAction.bind(this)}>
<DropdownButton>
<Icon name="more" />
</DropdownButton>
<DropdownList>
<DropdownItem name="navigate" icon="arrow_right"> Navigate </DropdownItem>
</DropdownList>
</Dropdown>
</div>
<NgIf className="headline-properties" cond={this.state.properties}>
<div>
<label> <Icon name="schedule" />
<input type="date" onChange={(e) => this.props.onTaskUpdate.bind(this, 'schedule', this.props.line, e.target.value)}/>
</label>
</div>
<div>
<label> <Icon name="deadline" />
<input type="date" onChange={(e) => this.props.onTaskUpdate.bind(this, 'deadline', this.props.line, e.target.value)}/>
</label>
</div>
</NgIf>
<NgIf cond={this.props.tasks.length > 0 && this.props.todo_status === "todo" && this.props.type === 'todos'} className="subtask_container">
{
this.props.tasks.map((task, i) => {
return (
<Subtask key={i} label={task.title} status={task.status}
onStatusChange={this.props.onTaskUpdate.bind(this, 'subtask', task.line)} />
);
})
}
</NgIf>
</div>
);
}
}
class Subtask extends React.Component {
constructor(props){
super(props);
this.state = this.calculateState();
}
calculateState(){
return {checked: this.props.status === "DONE"};
}
updateState(e){
this.props.onStatusChange(e.target.checked ? "DONE" : "TODO");
this.setState({checked: e.target.checked});
}
render(){
return (
<div className="component_subtask no-select">
<label>
<input type="checkbox" checked={this.props.status === "DONE"}
onChange={this.updateState.bind(this)} />
<span>{this.props.label}</span>
</label>
</div>
);
}
}

View File

@ -0,0 +1,197 @@
.component_modal.todo-modal{
background: var(--super-light);
background: linear-gradient(var(--super-light), var(--bg-color));
> div{
width: 90%;
max-width: 500px;
padding: 5px;
border-bottom-right-radius: 20px;
box-shadow: 1px 2px 1px rgba(0,00,0,0.05);
}
.modal-top{
margin: -5px -5px 0 -5px;
padding: 30px 20px;
background: var(--primary);
border-bottom: 1px solid var(--emphasis-primary);
color: rgba(0,0,0,0.5);
h1{margin: 0;}
span{
float: right;
margin-top: -10px;
.component_icon{
padding: 2px 0;
height: 22px;
cursor: pointer;
}
}
}
.nothing{
color: var(--light);
.big{
text-align: center;
padding: 40px 0;
font-size: 1.3em;
text-decoration: line-through;
}
a{color: var(--primary);}
}
.container{
margin: 10px 10px 10px 0px;
.sticky_header{
z-index: 2;
border-bottom: 2px solid var(--primary);
h2{
text-transform: uppercase;
font-size: 1.1em;
color: var(--primary);
margin: 10px 0 15px 0;
font-weight: normal;
span{
background: var(--bg-color);
font-size: 0.6em;
color: var(--emphasis-secondary);
float: right;
line-height: 1em;
vertical-align: middle;
display: inline-block;
padding: 3px 5px;
border-radius: 3px;
}
}
}
.list{
margin: 0px 0 40px 0;
z-index: 0;
}
}
.all-done p{
font-size: 1.3em;
padding: 10px 10px 10px 20px;
color: var(--light);
}
}
.component_headline{
.headline-main{
display: flex;
cursor: pointer;
border-top: 2px solid var(--super-light);
border-bottom: 2px solid var(--super-light);
margin-bottom: -2px;
padding: 5px 0;
&.overdue{color: var(--error);}
&.done{
color: var(--light);
&:hover{background: inherit;}
transition: color 0.35s ease-out;
}
.title{
flex: 1;
min-width: 0;
line-height: 32px;
> div{
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tags{
display: inline-block;
margin-left: 10px;
font-size: 0.9em;
.tag{
padding: 0 5px;
margin: 0 3px;
color: red;
background: var(--bg-color);
color: var(--light);
border-radius: 4px;
&:before{
content: '#';
color: rgba(0,0,0,0.2);
}
}
}
}
.component_icon{
height: 18px;
}
transition: color 0.1s ease-out, border-color 0.2s ease-out;
.title{
position: relative;
&:after{
content: ' ';
display: block;
width: 100%;
border-top: 2px solid var(--super-light);
position: absolute;
left: 0;
bottom: -7px;
transition: bottom 0.15s ease-out;
}
}
&.done{
transition: border-color 0.15s ease-out;
border-color: rgba(0,0,0,0);
.title:after{
bottom: 15px;
transition: bottom 0.25s ease-out;
}
}
}
.headline-properties{
> div{display: inline-block;}
input[type="date"] {
background: inherit;
border: none;
font-size: inherit;
font-family: inherit;
color: inherit;
}
.component_icon{
height: 20px;
float: left;
margin-right: 5px;
}
background: var(--super-light);
color: var(--light);
font-size: 0.95em;
padding: 5px 8px;
}
}
.subtask_container{
background: #fafafa;
color: var(--light);
font-size: 0.95em;
line-height: 0.95em;
padding: 5px 0;
.component_subtask{
margin: 5px;
label{
width: 100%;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
input[type="checkbox"]{ margin: 0 5px; }
}
}
}

View File

@ -29,6 +29,7 @@
"node-ssh": "^4.2.2",
"nodegit": "^0.18.3",
"path": "^0.12.7",
"react-sticky": "^6.0.2",
"request": "^2.81.0",
"request-promise": "^4.2.1",
"scp2": "^0.5.0",
@ -72,6 +73,7 @@
"mocha": "^5.0.4",
"node-sass": "^4.7.2",
"nodemon": "^1.17.1",
"org": "https://github.com/mickael-kerjean/org-js/tarball/master",
"prop-types": "^15.5.10",
"react": "^15.3.2",
"react-addons-css-transition-group": "^15.6.2",