feature (org): cool stuff for org mode users ;)
| @ -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) | ||||
|  | ||||
							
								
								
									
										90
									
								
								client/assets/img/arrow-down-double.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										90
									
								
								client/assets/img/arrow-down.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										90
									
								
								client/assets/img/arrow-up-double.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										59
									
								
								client/assets/img/arrow_right.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										55
									
								
								client/assets/img/calendar.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										55
									
								
								client/assets/img/calendar_white.svg
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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 | 
							
								
								
									
										52
									
								
								client/assets/img/deadline.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										60
									
								
								client/assets/img/download_white.svg
									
									
									
									
									
										Normal 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 | 
| @ -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 | 
							
								
								
									
										54
									
								
								client/assets/img/more.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										52
									
								
								client/assets/img/schedule.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										55
									
								
								client/assets/img/todo_white.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										114
									
								
								client/components/dropdown.js
									
									
									
									
									
										Normal 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> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										49
									
								
								client/components/dropdown.scss
									
									
									
									
									
										Normal 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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}/> | ||||
|     ); | ||||
| } | ||||
| }; | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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){ | ||||
|  | ||||
| @ -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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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(); | ||||
| } | ||||
| @ -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(); | ||||
|  | ||||
| @ -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;} | ||||
|  | ||||
							
								
								
									
										594
									
								
								client/pages/viewerpage/editor/emacs-org.js
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
| @ -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> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -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> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										252
									
								
								client/pages/viewerpage/org_viewer.js
									
									
									
									
									
										Normal 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> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										197
									
								
								client/pages/viewerpage/org_viewer.scss
									
									
									
									
									
										Normal 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; } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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", | ||||
|  | ||||
 Mickael KERJEAN
					Mickael KERJEAN